Every now and then I see a tweet poll asking what we do for state management in React. In many cases the options are constrained to Redux, MobX and more recently React Context + Hooks.
Of course the only correct answer is it depends.
But here's what I do for medium-sized CRUD-like single-page React applications.
- I don't use any state-management library. (no Redux, no MobX, no Recoil).
- I try to manage most of the application state with routes. This means having different URLs for different parts of the application, even if it's a single-page application. I use React Router for this.
- I differentiate between application/UI state and remote data cache. and use SWR or React Query to keep a cache of remote data.
- The rest tends to be small UI state "details" such as which modal popup is open, or the state of a form before submitting it. For this, I prefer to use
useState
oruseReducer
hooks, keeping state close to where it's used.
Application state in the URL
The application state must be kept somewhere. I can keep it hidden away in memory or I can expose it in the URL, so our users (and developers) can benefit from it.
- Better UX: users can bookmark and share links and search results with others
- Better DX: developers don't need to click around to get the app to a certain state every time they refresh the browser.
- Better documentation: Help pages can point the user to the exact part of the application they describe.
I try to keep most of the application state in the URL, and I use React Router to handle the routes.
Remote Data is not state: it belongs in a cache
I cannot stress this enough. Fortunately other people can explain this better than me:
UI state should be separate from the server cache (often called "state" as well), and when you do that, you don't need anything more than React for state management.
Kent C. Dodds
Here's an excellent article: Why You Should Be Storing Remote Data in a Cache (and Not in State) by Jason Ankers.
"Remote data is read-only. It doesn’t belong in the same location as our UI state."
In CRUD-like web applications, where the server is authoritative, I don't want the client-side copy of the data to become stale.
Considering all this, in most cases I don't need to customise the way remote data is fetched and stored. I can delegate all that to a library like SWR or React Query.
These libraries store the fetched data in a static cache; and there’s no need to resort to React Context to "share" the data with other components because all components consuming the same data are automatically rerendered when the cache changes.
At work, earlier this year we refactored one of our SPAs to use SWR and the result was a much simpler application logic. In addition, we now benefit from all the nice features that come with SWR such as “focus revalidation” and “refetch on interval”. The app has been up and running for months and we haven't experienced any issues.
Local UI state should be co-located
Almost everything that isn't caught by the above cases is local UI state such as the visibility of modal dialogs or the fields in a form before it's submitted.
For these cases I prefer to keep the state close to where it's used. I usually find myself using useState
and occasionally useReducer
.
Comments?
I would love to hear your thoughts.
- What do you do for state management?
- Can you think of a common case that is not covered here?
References:
- SWR by Vercel.
- React Query by Tanner Linsley.
- Why You Should Be Storing Remote Data in a Cache (and Not in State) by Jason Ankers.
- State Management with React by Kent C. Dodds.
- Lifting State Up. React Blog.
- Colocation by Kent C. Dodds.
- I first learned about SWR thanks to a video tutorial by Leigh Halliday: "React Data Fetching with Hooks using SWR".
Cover photo by Oshin Khandelwal on Unsplash
Top comments (9)
Personally I think the use of what state management library I use is largely dependent on the size of the web application I'm building.
For small and medium applications I don't think Redux or any other state management tool is worth the overhead and I usually find it easier to just stick with react state and hooks.
However when an application has a lot of state and especially state that needs to spread across multiple components in a way where it becomes hard to have it passed down this is usually when I turn to state management libraries.
I do agree with the cache usage argument although for some applications I think storage of data retrieved is needed if you need to transform it in some way.
Hi! Thank you for taking the time to read and comment.
We're not constrained to cache the data as it arrives from the server. Both SWR and React Query take any async function (i.e. a function returning a Promise) as argument. This means you can write a function that calls
fetch
oraxios
awaits the call, then transforms the data and returns it.This way cache will contain the transformed data instead; and we don't need to involve a state-management library only for that.
Ah I see that's quite a clean way of dealing with it I might have to look more into that. Is the idea to always use a cache retreval rather than holding in directly in state? Or do think there is a use case for holding it in state? (In your opinion)
How would you describe a moment when it's becoming hard? What does it mean?
For me Id use the example of having to pass loading state around. Usually loading state is modified in multiple different components but the loading overlay is quite high up in the component tree. It then ends up being tricky to modify its state in a clean way and not having to have multiple loading components. (It could just be the case I don't know how to handle this type of thing correctly though)
A (separate) concern not really covered here is complex state transitions, for which we found xstate to be really useful... xstate.js.org/
Yes. I have only played around with Xstate. I'm not sure is right for a CRUD-like app with simple client side logic.
Have you used it on production? What kind of app?
Just saw your answer.
We used it for a logistics app that suppliers use in their warehouses to scan bar codes to assign products to containers, containers to pallets, pallets to trucks etc. As you can imagine the client-side state transitions are the most complex aspect. All "delivery" data gets sent at once in the end. A bit like an online shop flow...
Most of the time me as well want to use something easy and without boilerplate. That's why I created npmjs.com/package/store-me
It's only for React app with pure components (Hooks) but hey, we are already there right? :)