From prop drilling to centralized global state management with react
It all started with amazing frameworks like react, vue, angular and some others that have had the brilliant idea of abstracting the application data from the document object model (DOM). React specifically, with your reconciliation algorithm and soon with the fiber architecture, rocks on how fast these layers (abstraction and DOM) are updated. With that we can focus on our components instead of the “real” HTML implementations, however from that also come some other new challenges, let’s put it in images:
That’s the classical prop drilling react anti-pattern, the process of going through the react component tree in order to pass properties between them. Higher order components or Decorators, if you are in a more object oriented style, give us more flexibility and some others architectural possibilities. We can now extract away that functionality which we want to share and decorate the components that need use it.
It’s all fine while dealing with small apps with few components interacting with each other, however when we have complex communication between a vast component ecosystem than this approach starts getting complicated and bug prone. From that reality our unidirectional data flow comes into stage:
Until here nothing new, but what if we take the concept and apply it using react context and hooks!? That’s why you here!
Main concept
The main highlight by now is our great and new friend react hooks, and your consequently functional approach:
Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class.
Then the center idea is to use the context API together with useContext and useReducer hooks to make our store available to our components.
import React, { createContext, useContext, useReducer } from 'react';
export const StateContext = createContext();
export const StoreProvider = ({ reducer, initialState, children }) => (
<StateContext.Provider
value={useReducer(reducer, initialState)}
children={children}
/>
);
export const useStore = () => useContext(StateContext);
We export from this file source code here a StoreProvider (responsible for making the context/store available in the application), that receives:
- the reducer function with the signature (state, action) => newState;
- application initialState;
- and the application content (children);
And the useStore hook that is responsible for getting the data from the store/context.
Even though the nomenclatures are different from now on I’ll reference our context as store, because the concept is the same and we can easily associate to our well known redux architecture standard.
The beauty relies on this simplicity:
- StateContext.Provider receives a value object (your current state);
- useReducer receives a function: (state, action) => newState and an initialState then any dispatch made from our app will pass here and update our application current state;
- useContext get our store and make it available in our application!
All the rest is just code organization and minor changes, nothing to worry about :)
Going into details
As a proof of concept, I’ve done this basic todo list application, check here the source code and here the live implementation, it’s a basic interface that contains a couple of component and the current state tree so then we can see the state modifications over the time.
The project structure looks like this:
The structure is pretty straightforward (action as we’d normally do in a redux application), I’ve moved the initialState from the reducers, because reducer is about state modification and not definition, besides that the store folder contains the already discussed react context / hooks implementation.
The reducer file has a quite different design:
import * as todo from './todo';
import * as types from 'actions/types';
const createReducer = handlers => (state, action) => {
if (!handlers.hasOwnProperty(action.type)) {
return state;
}
return handlers[action.type](state, action);
};
export default createReducer({
[types.ADD_TODO]: todo.add,
[types.REMOVE_TODO]: todo.remove,
[types.UPDATE_TODO]: todo.update,
[types.FILTER_TODO]: todo.filter,
[types.SHOW_STATE]: todo.showState,
});
The point here is just to avoid those huge switch statements usually seen in reducer functions with a mapping object, so basically for every new reducer we just add a new entrance in the mapping object.
But again, it’s all a matter of implementation the requirement here is that the function needs to have the (state, action) => newState interface as we’re already used to with Redux.
And finally but not least our component subscribing to the store:
import React from 'react';
import { useStore } from 'store';
import { addTodo, filterTodo } from 'actions';
import uuid from 'uuid/v1';
import Button from '@material-ui/core/Button';
export default props => {
const [{ filter }, dispatch] = useStore();
const onClick = () => {
dispatch(addTodo({ id: uuid(), name: filter, done: false }));
dispatch(filterTodo(''));
};
return (
<Button
{...props}
variant='contained'
onClick={onClick}
disabled={!filter}
children='Add'
/>
);
};
What comes next
The next steps will be related to middlewares and type-checking, how do we work here? Technically the middleware is a function called just before the dispatched action reaches the reducer, so the createReducer function above is a great place for that, and what about type-checking!? Typescript on it! And see you soon!
Cheers!
References:
https://github.com/acdlite/react-fiber-architecture
https://reactjs.org/docs/reconciliation.html
https://reactjs.org/docs/hooks-intro.html
https://github.com/vanderleisilva/react-context
Top comments (6)
So it appears that while reducer functions are technically separated in different files, they actually combine into one huge reducer. Is this correct? That would seem to make difficult the tracking of different reducers, in libraries like
reinspect
which makes reducers visible and named in redux devtools. I don't know about anyone else, but that is a powerfully useful tool once an application starts growing.Hey Chad, thanks for the nice feedback, you're right, all reducer functions are combined in only one. In terms of getting difficult to be tracked by third party libraries, I may not get well your point, but I'd say that is the opposite, as we already have with libraries like redux and mobX, the centralised state makes easier for these libraries to track this info. I'd be way harder if this information were scattered among different functions .. Actually that's one of the main goals of the central stage management, to facilitate the state data administration .. :)
ps. sorry for the late answer, I was a bit busy these last months ..
I wish I had this when I was studying for technical interviews!! Thank you 😊
then you have it, and you can not only answer but also question these kind of "standard answers" ;)
Great article! I found out another way to manage the global state with hooks, please let me know what you think about it: link.medium.com/70zBOczGG1
Thanks for the feedback, the react-context-hook component referenced by your article is another good example of how simple the concept implementation is. It goes accordingly to the main point of this article, which is basically to say that hooks open a huge miriade of possibilities when it comes specifically to global state management, then now it come to us (developers) the responsibility of exploring this and think outside the (redux) box ... :)