Building an immutable store from scratch with React
March 13, 2019
We’re going to create a store using React and Meiosis.
Meiosis is not a library, you can’t import it. It’s a powerful pattern to manage state and it works well with view’s libraries like React, Vue.js, Lit-html.
Implementing a counter app
Meiosis is a stream based pattern, for this example I’m using the Flyd stream library. Note that is possible to not rely on any dependency by implementing the stream yourself.
The first thing we can do is designing our interfaces.
export interface AppState {
count: number;
}
export interface AppActions {
increment(): void;
decrement(): void;
}
Pretty simple signatures, right?
The store is a simple object containing our initial state and available actions. Here is the real power of Meiosis, we’re dealing with plain objects and functions.
export const store = {
initialState: (): AppState => ({
count: 0,
}),
actions: (update: StreamUpdate): AppActions => ({
increment() {
update((state: AppState) => {
++state.count;
return state;
});
},
decrement() {
update((state: AppState) => {
--state.count;
return state;
});
},
}),
};
Note that actions safely mutate the state. It’s worth compared to the standard immutability approach that bring a lot of complexity between state transitions. Meiosis keeps this simple as stupid.
Now let’s see how to build the state stream, we need only two operators to create our stream: map
and scan
. The last one, scan
, is the stream equivalent of Array.reduce
function.
export type Stream<State> = flyd.Stream<State>;
export type StreamUpdate = flyd.Stream<UpdateFunction>;
export interface UpdateFunction {
(state: AppState): AppState;
}
const update = flyd.stream<UpdateFunction>();
const updateState = (state: AppState, patch: UpdateFunction) => patch(state);
export const actions = store.actions(update);
export const states = flyd.scan<AppState, UpdateFunction>(
updateState,
store.initialState(),
update
);
In summary here is how the above code example works:
- The
states
stream takes our initial state using thescan
operator. - The
update
stream emits our actions, hereincrement
ordecrement
. - The
states
stream handlesupdate
emission and patches the old state. - The new state is emitted in the
states
stream.
Next to this we need to pass actions
and states
as props to our React app.
render(
<App actions={actions} states={states} />,
document.getElementById('root')
);
The current state is available using the stream.map
operator provided by the states
props. Now, when an action is dispatched a setState
is triggered which automatically re-renders our view.
class App extends React.Component<AppProps, AppState> {
state: AppState;
props: AppProps;
constructor(props: AppProps) {
super(props);
this.state = props.states();
}
componentDidMount(): void {
this.props.states.map((state) => {
this.setState(state);
});
}
render(): JSX.Element {
const { count } = this.state;
const { increment, decrement } = this.props.actions;
return (
<div className="App">
<h1>count: {count}</h1>
<button className="increment" onClick={increment}>
Increment
</button>
<button className="decrement" onClick={decrement}>
Decrement
</button>
</div>
);
}
}
Benefits
With Meiosis we’re importing nothing to the view, actions and states are passed to our app as props. It means that our React app doesn’t rely on our store implementation. It’s transparent, state managers like Redux are doing a lot of stuff behind the scene. As you saw Meiosis can be fully implemented in a couples of 30 lines of code. It really forces you to think about how to design the state and not how to deal with tools.
- We have a root state in our app which is the single source of truth.
- The code is deterministic, we can run it many times it’s gonna re-render exactly the same thing.
- We know exactly what’s going to happen because all of the code was written by ourself.
Final note
I’m not trying to convince you to drop Redux or something else, but learning Meiosis can helps you to understand exactly what state managers are trying to solve. It’s also a good way to go deeper into functional programming.
I highly recommend you to visit the Meiosis website, they go deeper than I did, it’s really interesting. They also offer a small package to time travel across states like the Redux Chrome extension, it’s a good catch for development purpose.
Here is the complete working example of our counter app.