context + state = constate
~2kB React state management library that lets you work with local state and scale up to global state with ease when needed.
๐ Read the introductory article
๐ฎ Play with the demo
If you find this useful, please don't forget to star โญ๏ธ the repo, as this will help to promote the project.
Follow me on Twitter and GitHub to keep updated about this project and others.
npm i constate
import React from "react";
import { State } from "constate";
const initialState = { count: 0 };
const actions = {
increment: () => state => ({ count: state.count + 1 })
};
const Counter = () => (
<State initialState={initialState} actions={actions}>
{({ count, increment }) => (
<button onClick={increment}>{count}</button>
)}
</State>
);
Table of Contents
- Local state
- Global state
- Composing state
- Global initial state
- State in lifecycle methods
- Call selectors in actions
- Testing
You can start by creating your State
component:
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;
Note: the reason we're exporting
initialState
,actions
andselectors
is to make testing easier.
Then, just use it elsewhere:
const CounterButton = () => (
<CounterState>
{({ count, increment, getParity }) => (
<button onClick={() => increment(1)}>{count} {getParity()}</button>
)}
</CounterState>
);
Whenever you need to share state between components and/or feel the need to have a global state, you can pass a context
property to State
and wrap your app with Provider
:
const CounterButton = () => (
<CounterState context="counter1">
{({ increment }) => <button onClick={() => increment(1)}>Increment</button>}
</CounterState>
);
const CounterValue = () => (
<CounterState context="counter1">
{({ count }) => <div>{count}</div>}
</CounterState>
);
const App = () => (
<Provider>
<CounterButton />
<CounterValue />
</Provider>
);
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}>
{({ count, decrement }) => (
<button onClick={() => decrement(1)}>{count}</button>
)}
</CounterState>
);
Those new members will work even if you use context
.
It's possible to pass initialState
to Provider
:
const initialState = {
counter1: {
count: 10
}
};
const App = () => (
<Provider initialState={initialState}>
...
</Provider>
);
This way, all State
with context="counter1"
will start with { count: 10 }
Note: while using context, only the
initialState
of the firstState
in the tree will be considered.Provider
will always take precedence overState
.
As stated in the official docs, to access state in lifecycle methods you can just pass the state down as a prop to another component and use it just like another prop:
class CounterButton extends React.Component {
componentDidMount() {
this.props.state.increment(1);
}
render() {
const { increment } = this.props.state;
return <button onClick={() => increment(1)}>Increment</button>;
}
}
export default props => (
<CounterState context="counter1">
{state => <CounterButton {...props} state={state} />}
</CounterState>
);
Another alternative is to use https://github.com/reactions/component:
import Component from "@reactions/component";
const CounterButton = () => (
<CounterState context="counter1">
{({ increment }) => (
<Component didMount={() => increment(1)}>
<button onClick={() => increment(1)}>Increment</button>
</Component>
)}
</CounterState>
);
This is just JavaScript:
export const selectors = {
isEven: () => state => state.count % 2 === 0
};
export const actions = {
increment: () => state => ({
count: state.count + (selectors.isEven()(state) ? 2 : 1)
})
};
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");
});
type Action = () => (state: Object) => Object;
type Selector = () => (state: Object) => any;
type StateProps = {
children: (state: Object) => React.Node,
initialState: Object,
actions: { [string]: Action },
selectors: { [string]: Selector },
context: string
};
type ProviderProps = {
children: React.Node,
initialState: Object
};
If you find a bug, please create an issue providing instructions to reproduce it. It's always very appreciable if you find the time to fix it. In this case, please submit a PR.
If you're a beginner, it'll be a pleasure to help you contribute. You can start by reading the beginner's guide to contributing to a GitHub project.
- Side effects / async actions (#1)
- Middlewares? (create an issue if you find a use case for this)
- Debugger/devtools
- Memoize selectors
- Global actions/selectors
MIT ยฉ Diego Haz