context + state = constate
~2KB React state management library that lets you work with local state and scale up to global state with ease.
Install
npm:
npm i -S constate
Yarn:
yarn add constate
Usage
Local state
You can start by using local state. Make the state global only when you need it.
-
Create your
State
component:// CounterState.js import React from "react"; import { State } from "constate"; export const initialState = { count: 0 }; export const actions = { increment: amount => state => ({ count: state.count + amount }) }; export const selectors = { getParity: () => state => (state.count % 2 === 0 ? "even" : "odd") }; const CounterState = props => ( <State initialState={initialState} actions={actions} selectors={selectors} {...props} /> ); export default CounterState;
-
Wrap your component with
CounterState
:// CounterButton.js import React from "react"; import CounterState from "./CounterState"; const CounterButton = () => ( <CounterState> {({ count, increment, getParity }) => ( <button onClick={() => increment(1)}>{count} {getParity()}</button> )} </CounterState> ); export default CounterButton;
Global state
Whenever you need to share state between components and/or feel the need to have a global state, just follow these steps:
-
Pass
context
property to yourState
components:// CounterButton.js import React from "react"; import CounterState from "./CounterState"; const CounterButton = () => ( <CounterState context="foo"> {({ increment }) => <button onClick={() => increment(1)}>Increment</button>} </CounterState> ); export default CounterButton;
// CounterValue.js import React from "react"; import CounterState from "./CounterState"; const CounterValue = () => ( <CounterState context="foo"> {({ count }) => <div>{count}</div>} </CounterState> ); export default CounterValue;
-
Wrap your root component with
Provider
:// index.js import React from "react"; import ReactDOM from "react-dom"; import { Provider } from "constate"; import CounterButton from "./CounterButton"; import CounterValue from "./CounterValue"; const App = () => ( <Provider> <CounterButton /> <CounterValue /> </Provider> ); ReactDOM.render(<App />, document.getElementById("root"));
CounterState
properties
Overriding This is still React, so you can pass new properties to CounterState
, making it really composable.
First, let's change our CounterState
so as to receive new properties:
const CounterState = props => (
<State
{...props}
initialState={{ ...initialState, ...props.initialState }}
actions={{ ...actions, ...props.actions }}
selectors={{ ...selectors, ...props.selectors }}
/>
);
Now we can pass new initialState
, actions
and selectors
to CounterState
:
export const initialState = {
count: 10
};
export const actions = {
decrement: amount => state => ({ count: state.count - amount })
};
const CounterButton = () => (
<CounterState initialState={initialState} actions={actions}>
{({ decrement }) => <button onClick={() => decrement(1)}>Decrement</button>}
</CounterState>
);
Those new members will work even if you use context
.
Global initial state
You can also pass initialState
to Provider
:
const initialState = {
foo: {
count: 10
}
};
const App = () => (
<Provider initialState={initialState}>
...
</Provider>
);
That way, components using context=foo
will have that initial state.
Testing
actions
and selectors
are pure functions. Testing is pretty straightfoward:
import { initialState, actions, selectors } from "./CounterState";
test("initialState", () => {
expect(initialState).toEqual({ count: 0 });
});
test("actions", () => {
expect(actions.increment(1)({ count: 0 })).toEqual({ count: 1 });
expect(actions.increment(-1)({ count: 1 })).toEqual({ count: 0 });
});
test("selectors", () => {
expect(selectors.getParity()({ count: 0 })).toBe("even");
expect(selectors.getParity()({ count: 1 })).toBe("odd");
});
License
MIT © Diego Haz