React Router and Remix introduced automatic revalidation of data after mutations. I believe this is a paradigm shift and boosts productivity like nothing else available today.
In this article, I'll show you step-by-step why this matters and how to get out of what I call "state management hell" with the help of React Router or Remix.
Imagine you have a React SPA with a top bar. It shows the avatar of the signed-in user in the right corner or a "Sign in" button if there's no current user.
Your login page lives at "/sign-in" and includes the top bar and a form with email and password right below the top bar. After a successful login, the user is redirected to the home page at "/".
Now, imagine you are not using a library to handle routing. You have a simple router component that monitors the window's location and decides what to render based on that.
Because you know all pages will render the top bar, it lives in a separate component that is rendered higher in the hierarchy before your router component.
What happens after a successful login? The router component detects the change in the location and renders the homepage component. But there's something wrong.
Since the top bar is above the router in the render hierarchy, it will not be re-rendered. Thus, the user avatar will not appear, and the UI will be stale. You have to reload the page to see the avatar. I bet you're thinking of an app with this kind of bug right now 🙃
There are several possible solutions to this problem, and we'll talk about how React Router and Remix address this in a minute. But before that, let's look at the most common approach today: centralized app state.
You add the current user state to a React Context or state management library, read from it on the top bar, and write to it after a user signs in. Done. No big deal, right?
Wrong. It's a huge deal. You now have to make sure all devs in your team keep the structure of the state in mind and remember to update it when necessary.
More importantly, you must spend a lot of time designing the state. In the example above, we only need to know the current user. But a real app has hundreds or thousands of pieces of data as part of the state.
You have to constantly make the trade-off between separation of concerns and centralization in state design. If you put everything in one place, the devs need to keep a big structure in mind at all times.
If you have separate states (auth, clients, orders, etc), the team should remember which states to update after each mutation. Damned if you do, damned if you don't.
As apps grow, most bugs are related to state management. Diagnosing, fixing, or preventing them becomes costly, with the application state adding a ton of cognitive load to teams.
New features take longer to be delivered and often break unrelated functionalities. Separate teams need to coordinate closely even when working on different domains.
This is what I call state management hell!
Enters automatic revalidation
So far, we've been only talking about the state that lives in the browser. But if our app needs to store and fetch data from any kind of server, it also has a state on the server side.
In our example with a centralized state, both the browser and the server need to know who's the current user. Once we sign the user in, the server stores its state somewhere, and the React SPA somewhere else.
And then, every time we change the server state, we also have to update the browser state. Having two sources of truth is the origin of our state management hell.
Whenever we have two sources of truth, synchronization issues emerge over time. But unfortunately, we cannot avoid having separate browser and server states in today's world.
Due to the nature of web browsers, the only way to have a single source of truth is to do a full page refresh every time we make a change to the state. If we do that, the state lives 100% on the server.
But it's the 2020s, and we need a better UX than that. That's where abstractions like libraries and frameworks are useful. If we can't avoid having two sources of truth, let's at least hide this complexity from developers most of the time.
That's what automatic revalidation does. It lets us code as if there were a single source of truth, even though behind the scenes, there are two.
Let's walk through our example again, but now it uses React Router 6.4+ or Remix. Our top bar is rendered by a root route that will have all other routes nested inside of it.
Instead of relying on our custom application state, we'll use a loader on the root route to fetch the current user from the server and display it on the top bar.
Then, on the sign-in route, we'll use a Form and an action to sign the user in. The action does the authentication and redirects to the home page.
Because we used an action, React Router knows we are probably changing something on the server state. Then it does something magically simple: it runs all loaders on the page again.
That makes the root route re-fetch the current user from the server and re-render, now with the correct avatar at the top bar.
By assuming that if we're running an action we probably want to re-fetch all data on the rest of the page, Remix and React Router are solving our problem in the best way possible: by making it disappear!
Of course, there are exceptions. Sometimes our loaders take too long to run, and we want more granular control over what gets revalidated.
All of that and more is covered by both frameworks. Think of automatic revalidation as an excellent default that you can use for 90% of use cases and override whenever you need to.
But even if you have to do a little state management for 10% of your use cases, you're out of state management hell. Welcome!