I develop a small, internal facing UI, and it's been using Redux for awhile now. The store provides functionality for certain global concerns like API fetching and notifications, but it's a bit unwieldy with all of the connecting and mapStateToProps'ing that has to happen just to inject a basic data flow. The best alternative to using Redux as a global state manager is React Context (from a purely React perspective) but until recently had some issues to overcome.
React Context, introduced in early 2018, is a way to share data deep into a component tree, by wrapping that tree in a Provider, giving it an initial store / values, and then accessing / updating those values in the child components by accessing the context 'Consumer.' The original implementation involved calling that Consumer, and rendering its children as a function with props, the props being the original store/value object from the parent node. But keeping track of all that Provider/Consumer/render propping gets clunky, and results in false hierarchies inside consuming components.
Updating data received from context, too, is tricky. Most people solved this by passing callback functions down with the context values, and using those to pass changes back up. But pairing data with callbacks like that is a little ugly, and it means that every time your data tree updates, it re-instantiates those callbacks with it. Redux's reducers provide a much cleaner way to update state, listening for event triggers that get fired by actions in the component, and updating the part of state relevant to that action. Until hooks, however, integrating reducers and context was a bulky marriage of technologies.
When hooks were introduced at the React Conf I attended in 2018, I saw their usefulness, but didn't understand why people were saying it was a Redux killer (it's not, necessarily, but that's a topic for another day). But when I discovered hooks like useContext and useReducer, things started to click into place. With the useContext hook, you can extract the context values without a consumer or having to use render props, and with useReducer you can extract both state and dispatch without much of the overhead needed by Redux.
Armed with these new tools, I decided to create my own global store/state management system, to rid myself of Redux once and for all (until I discover down the road that I actually do need it, but we'll let future problems live in the future for now). After about four or five iterations, I finally came on a pattern that made the most sense to me, and happened to eliminate hundreds of lines of code, as a nice side effect.
Before we get into the details, I want to give credit where credit is due - this article by Eduardo Marcondes Rabelo and this one by Tanner Linsley were foundational to my understanding of how to put these pieces together, and I borrow heavily from their ideas. I've also seen similar implementations here and elsewhere. The takeaway here is that there's more than one way to peel an orange, and you should choose the way that's most… appealing to you.
For an example, we'll make a very simple React application that lets the user view and refresh data from a 'stocks' API, using both state and actions from a global store. The folder structure will look something like this:
Notice the 'store' folder contains a folder for the stocks' API reducer and actions, similar to how a typical Redux project might be structured.
Our entire application will be wrapped in a StoreProvider to give every child element access to the actions and state, so let's create our index.js to start:
Again, this is a similar construct to how a Redux store would be placed at the top of an application:
The types, reducer, and actions also look very similar to Redux:
Next, let's create a helper function called 'combineStores' that will combine all reducers, combine all initial states, and return an object with both:
We'll create two other files in our store folder - a rootReducer to give us a structured object with all the reducers and initial states (namespaced according to their respective folder names), and a rootActions to provide a similarly namespaced object for all actions in the store:
To bring it all together, we'll create the StoreProvider to wrap our application in, which will provide access to all components with the global state, actions, and dispatch:
There's a few things going on here - first, if you're not familiar with hooks like useReducer, useMemo, and useContext, the React hooks API docs are a great place to start. There are three important features - the useStore function (which is actually a custom hook) returns the values from the global State context, and the useActions hook returns the namespaced actions object (more on that in a bit). The store provider is actually three nested contexts, State at the top to provide actions and dispatches access to the global state values, Dispatch, then Actions, so actions will have access to the dispatch. I'm keeping them as separate contexts here, because when the state updates (as it will do when an action is fired off) it won't reinitialize the actions and dispatch. Dispatch doesn't necessarily have to be its own context - it could just be a value passed into the actions getter, but I like to keep it available in case there arises a need for a child component to directly dispatch something.
Before we look at the store being used inside of a component, let's first understand what useStore and useActions are actually delivering. When we call useStore and useActions, they give back objects something like this:
Let's go ahead and create our App.js which will hold our Stocks component:
Now let's create that Stocks component:
You can see we're pulling in the useStore and useActions hooks from the store, getting the state values under 'stocks' from useStore and the global actions object from useActions. The useEffect hook runs every time the component updates, but because we pass in an empty array as its second parameter it only runs on mount. So when the component loads, a call to the 'fetchStocks' action will be made, and then again any time the user clicks the 'Refresh stocks' button. For a comparison, let's see what that component would look like if we used Redux:
Things would get even more complex if we allowed the user to modify the existing state (another article for another time).
The choice to use a large state management library like Redux vs some kind of custom variant like this is at least partly subjective, and will depend on the different needs and scale of your application. Bear in mind, too, that tools like context and hooks are brand new, and 'best practices' is still in the eye of the beholder. That being said, feedback is strongly encouraged - this implementation is really just a first effort for something that will hopefully be much more robust in the future.
Top comments (14)
Do you have any public code I could look at? It's hard to tell exactly without seeing the context :)
Got it working now. I took out params = {} thinking it wasn't required
That's great! Glad it worked
Thanks about your articles, do have a repo for this article?
I never made one, sorry! I may one day and if I do I'll post it here, but I'm more likely to write an update to this that's a little less heavy handed with context, I'll have a repo with that one if I do.
Thanks for the article. It seems really a pain to implement redux... You have any repo with best practices architecture redux/hooks to recommend ? a simple one if possible, thanks :)
I don't have a public one, yet! But I'll post something here when I get to working on a big refactor of a public project I have. I've learned a few things since I wrote this and think there are some cleaner ways to do stuff, especially around actions. I do agree that redux is a lot of implementation detail, and while this article cuts down on some of that it's still a lot of boilerplate that I'd like to reduce, without putting a lot of work on the consuming component.
Thanks, looking forward for a repo :D
By the way, I tried this redux dev tools package to trace hooks activity, really cool: github.com/troch/reinspect
i couldn't seem to get this to work. Where did you add it? I tried around the main app or storecontext but it didn't show that it updated state in the devtools
hit there is any way to access the state tree in the actions?
like store.getState() ?
Sorry for the late reply. One way would be in the 'getRootActions' method in the StoreProvider component, to pass another parameter after dispatch, 'state', and then pass that into bindDispatchToActions to be available to any of the actions it iterates over. It's kind of clunky, but quite honestly this whole approach here is a bit heavy. I think I might, especially for a smaller app, just leave actions context out entirely, export some basic action functions from a file and just import those anywhere they're needed, passing in dispatch at point of use.
Using this architecture does it mean that any changes to the store will cause re-renders to all the components even if they aren't interested in a piece of state?
I tested this out on a project I used to work on and didn't see re-renders, although it may depend on the implementation - I believe (although I could be wrong) that the way context works here in conjunction with the reducer hook means that state changes don't cause re-renders in anything but the components using that piece of state.