6 things I wish I knew about state management when I started writing React apps
I have a lot more blog posts like this I plan to write. If you like this, follow me on Twitter to make sure you don’t miss any.
Think about the most complex frontends you’ve used. Frontends that made you wonder — “how did they create this”?
Here are some of mine.
What makes these frontends complex?
State management. The frontend “knows” a lot of things, and these things interact with each other in non-trivial ways.
So in my view, state management is the core problem when developing a UI.
Here are some things I wish I knew about state management earlier.
1) State management is how you mitigate prop drilling
State management matters
To build a non-trivial React application, you need to consider state management, in my view.
You don’t need to use a third party library, you can use Context, but you do need to figure out how to store global state that can be accessed anywhere in your application.
Example: dark mode support
For example, say your app has a dark mode. All your rendered components must know what theme is on, so they can render the UI in the right color.
Other examples of global UI state include whether the user is logged in, the the logged in user’s username, and the value of the global search box.
Prop drilling isn’t the answer
Context didn’t exist when I started learning React, so to solve this problem, I prop drilled. As your app gets larger, prop drilling becomes impractical.
State management is the answer
Why don’t more people know this?
I had no idea state management was the solution to the prop drilling problem until I learned Redux, out of curiosity. “State management”, the term, doesn’t seem related to our problem. What is state? Why does it matter?
To make things worse, many articles say you can build non-trivial web apps without state management, which initially deterred me from even learning about it.
2) State management is how you update your page after creating/updating data, without a refresh
Say you’re writing a todo app. After the user creates or renames a todo item, you’d like to update only the todo list component — but not refresh the page. How do you do this?
The solution is to store the list of todo items in a global store in the client, along with a method for re-fetching the list of todo items from the server and updating our store.
Then, your todo list component reads the list of todo items from your global store. And after the user creates or updates a todo item, call the method for re-fetching the list of todos, which updates your global store, which updates your component.
I think of the list of to do items on the client as a client-side cache, a subset of the data on the server.
3) Manage your state maintainably by storing it in the right places
Don’t just put all your application state into whatever state management library you choose.
Instead, recognize that your application has several different kinds of state. I’m inspired by James Nelson’s post on this topic.
- Data + loading state: the list of todo items your frontend renders and whether the list is loading. Put into Redux/MobX/etc.
- Global UI state: whether the user is logged in, value of a global search bar. the server doesn’t store this data at all. Put into Redux/MobX/etc.
- Local UI state: whether an dropdown is expanded, for example. The rest of your frontend doesn’t care about this. Use component state
- Form state: the values of fields in a form. This is a subset of local UI state. Use a library like Formik to treat the form as a controlled component
- URL state: the route the user is on now. Read and update
window.location; don’t create a second source of truth
- Page state: you have a page whose components interact with each other in a complex way, but not with components on other pages. Create a Redux/MobX store just for the page (or pass down a plain JS object with Context)
4) Learn other state management libraries in addition to just Redux
My struggles while using Redux
Redux is probably good enough for your application. But I didn’t like using it.
- Why do I need to worry about normalizing the data from my server on my client? Why do libraries like Redux ORM need to exist? I don’t want to re-implement a bunch of my server-side code on the client.
- Why do I need to touch multiple files and write a lot of boilerplate code in order to add a simple feature?
- What is an action/action type/action creator/reducer/store again?
- I understand the benefits of writing immutable, functional code, but writing Redux reducers feels needlessly unintuitive.
- I’d like to simply make an API call in an action and update my Redux store without learning and using Thunk or Saga.
Switching to MobX
Then I learned MobX.
- Each piece of state in a store is just a class variable.
- Actions are just methods in the class with an
- You can make API calls in your actions just like you normally would; just wrap the code that updates your state after the call with
- It supports observability, which I don’t use much myself, but is a useful tool.
Find the right library for you
But keep learning different libraries until you find one that fits you.
5) Your React app has two layers
State and view layer
I think of a React app as having two layers:
This is what people mean when they say “UI is a function of state” and “React is just a view layer”.
Benefits of this mental model
By embracing this paradigm, you can write testable, functional code, decoupled from your UI, for managing your state.
Keep in mind…
Of course, in practice, not all your state should be in your global state, as I write above. But this is a useful mental model anyways.
6) Shared vs non-shared components
Fetching data in all my components
When I first started writing React, I put my data fetching code in my components.
But this meant I couldn’t use a component in another place for its UI only.
Fetching data in my container components
Then, I read Dan Abramov’s article on presentational vs container components.
At first, I put my data fetching code in my container components, each of which rendered a presentational component.
But I realized I needed to put my data fetching code in MobX in order to update the UI after creates and updates (see #2).
Fetching data in MobX actions
Next, I put my data fetching code into MobX actions. Each container component connected to my MobX store and rendered a presentational component, passing data and methods from my MobX store as props as needed.
Unfortunately, this forced me to write a lot of boilerplate container components that effectively did nothing.
What I do now
I concluded that the right distinction is shared vs non-shared components.
- Shared: Components you’ll need to render in more in than one place in your app. Can be either presentational or container. Examples: UserSelect, Button
- Non-shared: Components you’ll only render in one place. Examples: UserTable, GlobalSearchInput
The presentational vs container component distinction assumes that you want to re-use presentational components, but not container components. I disagree.
You usually want to re-use some presentational components and not others, and you want to re-use some container components and not others.
Get in touch
Have a comment? Question? Leave a comment below!
Also, there’s a lot more great content coming. Follow me on Twitter to stay updated.