A better approach to managing state in your webapps
Unidirectional data flow. You’ve heard of that one.
It’s what made React famous. The idea that your data always flows in one direction:
- starts in your data store
- flows into your views
- goes back into your store through actions
A single source of truth for your UI. Know the state, know what’s on the page.
NOTE: This is a cross-post from my newsletter. I publish each email two weeks after it’s sent. Subscribe to get more content like this earlier right in your inbox! 💌
Your views are a pure representation of state. That makes your code easier to debug, easier to think about, and easier to develop.
Render your view (component) purely from props, make changes only through a clear API, wait for the engine to re-render with updated props. Got a problem? Fiddle the props. 👌
Even your state becomes easier to understand. It’s a state machine.
You have states (potential values of all the fields), and you have arrows between them. The actions that change states.
Sure, a lot of those fields have infinite potential values, so you’re actually working with a Turing machine, but bear with me.
Single source of truth
This success stems in large part from the idea of a single source of truth.
You see, when you can’t be quite sure which object holds the definitive state you’re rendering from, life gets quite hard. Are you rendering from a property of your view? Is it the data model? The value in your input box or the value you have in a model?
When you make a change, you have to track down everywhere it’s saved. Miss one, and you never know what might go wrong.
You spend most of your time chasing down obscure bugs. No good.
With unidirectional data flow, all your problems are gone forever. Make a change, change some state, re-render. 👌
And then it goes wrong
And then you get carried away. You use your main data store for everything. Whether you’re using MobX, Redux, or hoisting state up the tree, your code likely suffers from this problem right now. Admit it. Mine does.
You end up with a mess like this 👇
This is a huge data store storing all sorts of data. Important data, irrelevant data, data nobody cares about.
response groups? Each of those is an API call. Components keep track of what's up through state.
Why does every component in the whole app care? They don’t.
Out of all those fields, the only parts everyone cares about are:
- the list of processes
- the list of projects
- the list of secrets
- the list of teams
- current user session
- current router state maybe
Everything else? Irrelevant. Keep it in local state.
Local shared state for widgets
When you build a widget, or a section of your app, it’s often built out of several components. Each of these must know what all the others know.
If you have a form, fields have to render differently when there’s an error. Dropdowns are different when open or closed. Search values in lists often change how you render items.
All this should go into local shared state.
Think of your form as a self-contained unit. It needs its own data store, not to piggy-back on your global store.
Use Redux, MobX, React Context, or hoisted component state. Doesn’t matter. But you should keep it contained.
All your errors and intermediate values — anything that doesn’t affect the application as a whole — should live here. When you’re done generating the final result, you communicate it up the chain of command using well-defined actions.
Global state for your app
In essence, what you end up with is a tree-like structure of data stores. Tiny local stores handle specific parts of the application. The main global store acts as a communication hub where everyone stores their most important data.
For example, a login form with a profile icon in the header.
The form has a small store that takes care of field values, error states, loading spinners, and such.
The header icon has a store that, say, holds a flag for whether you opened a profile menu. Maybe the number of notifications.
But there’s an important piece of data both widgets need to know: the Current User.
So you’d make a global store that holds the current user.
When the login form succeeds, it calls an action and updates the current user.
The new information flows down from the global store into our profile and login widgets. The profile widget re-renders and shows your avatar.
The login form re-renders and shows a big fat You’re Welcome sign.
But until the login succeeds, there’s no reason to bother the profile widget. And when you open your profile dropdown, why bother the login form with that?
Think of it as splitting your state machine into multiple sub machines. You keep the unidirectional data flow between big components. And you use unidirectional sub flows for locally contained stuff.
Try it. Your future self will thank you.
Learn While You Poop Series 2 is all about state
Learn While You Poop Series 2 starts today. I’ve got a buffer of 5 episodes so we should be good.
Here’s the plan:
- Talk about the basic approach described above
- Build 4 to 5 different versions of the same app
- Use a different state management library each time
- Become experts at managing state
You should consider subscribing for $19/month to get extra content and a daily reminder to learn.