Press enter or click to view image in full size
Do you write a reducer, 3–5 action creators, and async action for each small piece of data you want to fetch from a backend? If you think that it’s a bloated bunch of files where “business” code is lost among constants, reducers, connect/dispatch/getState — you are in the right place, I’ll show you how to get the same benefits in much more cohesive code with pure React.
This article is a refactoring session. I assume that the reader has a solid understanding of both Redux and React Hooks.
Starting point
Imagine you are building a regular web app using react-redux. One of the features you might want — maintain session info in Redux state and consume it across your app. Here is a typical example of state consumption you might have:
You might have even more if you use fine-grained selector or mapDispatchToProps.
use Hooks
react-redux provides hooks API, the same code could be written like that:
We didn’t do anything special. We just used the React Hooks API instead of HOC API from the same library (connect is a Higher-Order Component). It eliminates some boilerplate associated with Redux state consumption. Just call useSelector and you are done.
Components should not know where data is coming from
After the first step, we ended up using the useSelector function imported from react-redux. It’s pretty ok if you are just consuming redux state and don’t want any side effects. But what you really want to consume here — the state of the session feature. Redux is just an implementation detail, and we can hide this implementation detail into the feature module. It’s not just “cleaner code” (I don’t like an idea about clean code in a vacuum). It’s an important step that gives us some space for refactoring and moving things around.
At this point, our view component (Hello.js) does not know anymore how the session is loaded. It just uses a custom hook provided by session.js. But the session module itself still depends on redux actions, reducer, and some other boilerplate. Remember situations when you have to write componentDidMount just to dispatch fetch action to redux? I always wanted redux to magically understand that I’m consuming this particular part of the state and fetch it. I won't write this code here, we are going to throw it away anyway.
You don’t need Redux
Components consume session only via useSession hook. if you replace redux with something else, components don’t care. So let’s do it. The Hello component remains the same and will not change anymore, so I post only changes to the session module. I start with a simple implementation of the module, with some gaps, and improve it over the next refactoring iterations.
You don’t need anything more to make it work, that’s it. Also, it solves the popular redux problem — when it’s time to initiate a data fetch and who is responsible for that? With custom hook approach the hook itself is responsible, no additional boilerplate.
Don’t recalculate what you already know
The snippet above suffers from two shortcomings:
- Any time you consume session in the new component, it calls the
fetchSessionfunction, which might be expensive to calculate, i.e. it might do network requests. - The first time a new component is rendered, it is rendered with the “loading” state — initial state of the session.
Let’s fix that! See comments in the code for explanations
This code looks a bit hacky but anyway, it solves the problem. The session is fetched at most once and no extra rerenders occur after the session is fetched. Also, the code is highly cohesive now — just one file for the entire session management.
But wait, isn't it an oversimplified case?
Well, it is. A more complex example would be if you use the session feature to track login/logout as well, not just looking for the current state. In the redux app you would want to dispatch actions for that, and use some kind of middleware to handle async request (usually redux-saga or redux-thunk).
Get Vadim’s stories in your inbox
Join Medium for free to get updates from this writer.
Let’s write a Component that will use login/logout functionality without redux.
We can use components code now to understand what API we want session to expose. It should have one hook useSession — the same hook as before, but in addition to that it should expose login and logout functions, that return promises. In case of a successful invocation of any of those two, useSession should trigger a rerender of its consumers. You might want to use React Context to achieve that, but let’s try to mimic context functionality first:
As you can see, you could create an imperative API and consume side effects of this API using React hooks. No Redux or React Context is needed. The error during login is handled by the initial caller, and the session does not change in case of error.
Use context to deliver one change to multiple consumers
We used an array of consumers in the previous example. It works, but React can not optimize much here and you might have unpredicted state changes on an unmounted component. Imagine that state change for one component will lead to the unmount of other components. We didn’t handle that in the previous example, but React Context handle such a situation out of the box.
One shortcoming of the default React Context behavior is that Provider is mounted and evaluated independently of whether it is actually used by any component or not. To overcome that we could postpone data fetching until our custom hook is called.
useSession hook calls the setHasConsumer callback passed from Provider, and it’s a signal for Provider to fetch the data.
Summary
I hope I helped you not to bloat your App with Redux boilerplate for numerous small features. You might have pretty much number of Contexts in your App — different Context for each independent feature.
I did not cover one more topic here — how to organize services when one service depends on another. It’s pretty easy if you don’t care about performance much — just use hook exported by one service inside the provider of another service. But you will lose performance gained by setHasConsumer optimization. Dependent service will always have a consumer. And how to overcome that is subject for another publication.
UPD:
Here is the second part of the story, where we build reusable library for state management and fix shortcomings outlined above — Using React Hooks for global state management.