If you’re not familiar with hooks, go read the intro (Click Here)
Still with me? Good. Over the last couple months I’ve been fiddling with React Hooks. Hooks massively reduce a code base’s footprint, allow inline expression, logical separation, and a ton more. However, they also open up the possibility of something pretty neat: A new way to manage global state.
Historically we’ve had two big state management tools: Redux and MobX
Now, I had a few gripes with Redux right off the bat (If you’re reading this, I’m sorry Dan). I found it wasn’t a great abstraction because I had to trace through the whole reducer/dispatcher flow, which became extremely painful as the code base grew. To be honest, it felt a lot like those little vacuum tubes they used to shoot messages around.
That is, most of the time I thought to myself “Wait, where the fuck did my data go again?”
So, I pretty quickly moved to MobX. MobX was awesome; at least in theory. Create a store, observe it, mutate the values, component updates. Voila.
Except, sometimes you forget an observable, or a small quirk about the framework, like when/how autorun fires or that assigning arrays to observables destroys their observability. Plus, you end up with a dirty, unreadable decorator sandwich if you’re using multiple stores.
Again this really fell apart in large code bases, where MobX was extremely difficult to trace, since mutating the value causes re-renders everywhere and you can mutate it from any subscribed component. Even when I was using it right, it felt like getting hit with 40 different dodgeballs.
So if it’s not Redux, and it’s not MobX, what is it?
Enter: Hooks
Hooks is the abstraction I’ve been waiting for: When a variable is updated, re-render the component. It’s state, but compartmentalized.
So what if we had a component which managed our higher level state for us, and let components just be the view?
The answer, I propose, is something I’m calling Hookstore. It’s a MobX style observable store which exposes a hook containing two variables:
The stores emitted data
A series of predefined actions to mutate that data and cause re-renders
In practice, it looks something like this (TypeScript warning):
Let’s break this down: We’re creating a component which has a useState hook which tracks whether a user is signed in or not. We’ve additionally created an “actions” object: These actions mutate the state and cause the component (And any component which uses our hook) to re-render. Note: Classic function declaration is used here so that actions may call other actions (e.g signIn may call some this.RefreshData() action)
Since hooks can only be invoked inside React components, we’ve additionally created a context to store our values and our actions. We extract them using the useContext hook.
Finally, we mount our Hookstore inside of our Render tree. This is done so that the component mounts and the state and actions are created.
Now that our store is setup, simply call the useUser() hook and invoke any of the actions you’ve created. Here’s a (really weak) landing page
As you can see, we can now access whether a user is signed in or not and invoked the functions which mutate the stores state. When a user clicks on Login, the stores action will be fired, updating the state, causing a re-render of this component.
This is an incredibly powerful pattern that allows you to build complex yet traceable stores. Relevant extensions include:
Adding useEffect to fetch remote data
Caching locally using localstorage/sessionstorage
Using a stack to propagate modals/errors/frames
Here’s an example of the last bullet for a small React Native app I’m working on. It exposes a push/pop function and allows screens to be shown to the user and dismissed from anywhere in the app, including the mounted component.
Closing Notes
I wanted to share it with people in case they find this information helpful. I’ve struggled for years now to find a decent global state management solution, and I think this checks pretty much all the boxes.
My only gripe with it ATM is the part where the Store needs to be added to the Render Tree. If you can figure out some way around it, I’d love to hear about it in a comment or on Twitter.
Thanks for reading!