(you can also read this here)
I was at a Remix meetup in Utah last night. At the meetup, Kent gave a talk in which he said that one great thing about Remix is that he doesn't have to think too much about state when using it. Afterwards, someone at the meetup asked me what he meant. It seems weird that you wouldn't have to think about state. Isn't state, like, a huge part of building an app?
To answer this question, it's important to know that it's not that you don't use state when building a Remix app. Rather, the framework just takes care of a lot of it for you. Here's what I mean by that.
A huge source of state in React applications is server state. The typical way to handle server state is to fetch it from the server with JavaScript and then use React Query or something similar to cache the resulting data client-side. All of that requires thought on your part. You need to understand how to use whatever caching library you're using. If you make a data mutation, you have to keep track of which queries/data to invalidate. You need to show error messages if there's an error. It's a lot to think about.
With Remix, you don't have to think about any of that. All you have to do is return the data you need in your loaders and grab that data with useLoaderData
. When you send a mutation, you don't have to invalidate anything; the data on the page gets updated automatically. When you define CatchBoundary
and ErrorBoundary
components for error handling, you don't have to think about when to render them; Remix will render them at the right time for you.
So how exactly does this work? Where does Remix store the data for the page? And how does Remix know when to update it?
If you don't have JavaScript on the page, then there's nowhere for Remix to store the data. The HTML page itself is effectively the "store", and when you mutate data with a form, the page is refreshed, a server-side render happens, and you get refreshed data. This is how browsers work by default.
If you have JavaScript on the page, then Remix stores your data in a global context and provides a few ways for you to access it.
The first way, as mentioned, is useLoaderData
. This hook will grab the data returned by the loader for the specific route you call the hook from. For example:
// routes/recipies.tsx
export const loader: LoaderFunction = () => {
// return some data
}
export default function Recipies() {
// This will grab the data returned from the above loader.
const data = useLoaderData();
// Or, you could move the `useLoaderData` inside
// `RecipieCard` instead of passing `data` as a prop.
// Since the `Recipies` route is the closest to
// `RecipieCard` in the component tree,
// you'll get this loader's data.
return <RecipieCard data={data} />
}
The second way is useMatches
. This hook will give you all the data for every route that matches the current URL, so you can grab the data for any route that is currently rendered on the screen.
There is also a third way that might be added to Remix in the near future, called useRouteData
. This hook will allow you to get data from a specific route by passing a route id.
You can also grab data from any loader (even ones that are not part of the current route) with useFetcher().load
. However, unlike useLoaderData
and useMatches
, this data will not come from the global context and instead, useFetcher().load
will send a network request to get the data and store it locally, just like you might do with fetch
.
If you submit a form with <Form />
or call an action with useFetcher().submit
, Remix will call all the loaders for the current route and update the global context for you. You don't have to think about it! What's cool about this is that Remix is just emulating regular browser behavior here. Again, if there were no JavaScript on the page, the browser would do a full page refresh, which would call all the loaders for the current route, and you'd get a fresh HTML document with fresh data. This is exactly what Remix is doing, just with JavaScript so there's no page refresh.
And for error handling, all you have to do is export an ErrorBoundary
component for unexpected errors, and a CatchBoundary
component for errors you throw yourself, and if there are any errors, Remix will display the error UI in place of the regular UI for that route automatically, without you having to think about it.
Doing things the Remix way does require a bit of a mindset shift. You have to think about data and errors in terms of your routes. Whenever you call useLoaderData
, you will get the data for the nearest route in the component tree. The ErrorBoundary
and CatchBoundary
display in place of the UI for whatever route they're defined in. But reframing things in terms of routes enables a lot, and it's what makes Remix so special and easy to use.
Thanks for reading.
Top comments (7)
Great explanation, now I can sleep
If this page was using Remix, does it mean when I write comment and hit "Submit" is will reload all the data of current route, so article body, likes/unicorns, author info, read next section - everything on the page will be re-fetched?
Yes, just like if there was no JavaScript on the page, default web behavior.
Routes can opt-out of being reloaded though with
unstable_shouldReload
so your unicorns won't be bothered if they don't need to be (unstable just because we're still learning what the inputs to this function need to be).You can think of remix loaders like queries. Instead of actions needing to know which queries need to be invalidated, the queries themselves can decide if the action invalidates them or not.
Routes is very confusing term which has many meanings, so I suppose they may mean something different in Remix
I mean in React routes are sometimes confused with "screens" aka "pages", in node.js they are confused with "controllers"
In my understanding, route is a url path, this post has a route like /:authorId/:postId
So could you clarify, if I don't want unicorns to update on posting comment with Remix but they are displayed at the same post path /zachtylr21/some-thoughts... which is matching the route, what do you mean by
So unicorns have to be shown at a different route, but what does it mean that comments are in one route, unicorns are in different route, but they all are displayed on the same page
If I get all correctly and route means the string '/:authorId/:postId' then I have doubts on Remix to be adopted where optimizations matters.
Great post, thanks! Bit of additional trivia. When multiple fetchers data revalidations are inflight, Remix will commit them along the way as they land, but only if they are fresh. If a revalidation lands that was fired later than any that are still in flight, it will cancel those requests to ensure that your app never gets into the wrong state due to race conditions.
Most devs don't notice these bugs because their local server handles requests in order, but out in production you are not guaranteed that same behavior.
Demos we've ported to Remix written by the teams behind other frameworks (including the React team) even exhibit these bugs! Very few devs deal with it, but with Remix it's handled automatically.
Oh that's awesome, thanks for the comment!
Thanks for your article. It's now 2023, and it has provided me with a lot of knowledge