Data fetching
This module is all about data fetching in React Router!
Making the screen look pretty is fun, but making it work is even more fun! In this module, we will explore
how to fetch data in React Router, and how to use that data to render your app.
There are 2 main module exports we will explore in this module:
loadersclientLoaders
These two exports are essentially functions that are called by React Router to fetch data for your routes, nothing
more, nothing less!
There are a lot of utilities and helpers that React Router provides to make accessing and fetching data easier,
but at the core, it's just about these two exports!
In this module, we will explore both of these exports, and how to use them to fetch data for your routes.
Before we jump into them it's important to understand what happens when you navigate to a route in React Router.
The following scenarios I describe are per route segment basis, meaning if you have a route like this:
/app/products/:productIdThen the scenarios I describe below will happen for the
/app & /products & /:productId route segments in parallel.
After they all are done the data is combined and passed to your client where it's further processed and passed to your components. This is done by
React Router under the hood, so you don't have to worry about it. They use turbo-stream to serialize/deserialize the data to/from the client so you can
send promises, dates, and other complex data structures across the wire.๐ More information on turbo-stream: https://github.com/jacob-ebey/turbo-stream
There are two distinct cases when fetching data in react-router:
- Document request: This is the initial request to the server when you land on the website (think of full page reload or clicking on a link to your website)
- Navigation request: This is when you navigate to a new route within the app (think of clicking on a link within the app)
There are three possible scenarios for both of these cases:
- You navigate to a route that has both a
loaderand aclientLoader - You navigate to a route that has only a
loader - You navigate to a route that has only a
clientLoader
Scenario 1: You navigate to a route that has both a loader and a clientLoader
Document request
In this scenario, React Router will first call the
loader function on the server side to fetch data for the route.
Depending on the result of the loader function, assuming it didn't throw, the data is returned to the client.From here there are two possible scenarios depending on if the
clientLoader function has the hydrate property set to true or not.- If the
clientLoaderfunction has thehydrateproperty set totrue, then React Router will call theclientLoaderfunction on the client side, passing the data returned from theloaderfunction as an argument. The result of theclientLoaderfunction is then used to render the route. - If the
clientLoaderfunction has thehydrateproperty set tofalse, then React Router will skip calling theclientLoaderfunction on the client side, and will use the data returned from theloaderfunction to render the route.
In both of these scenarios, subsequent requests trigger the
clientLoader function to be called again on the client side and it's
in charge of fetching the data from the loader function or any other data source.Navigation request
In this scenario, React Router will first call the
clientLoader function on the client side to fetch data for the route.
The result of the clientLoader function is then used to render the route.Scenario 2: You navigate to a route that has only a loader
For this scenario the behavior is the same for both document requests and navigation requests, and is as follows:
- React Router will call the
loaderfunction on the server side to fetch data for the route. - The data returned from the
loaderfunction is then used to render the route.
Scenario 3: You navigate to a route that has only a clientLoader
For this scenario the behavior is the same for both document requests and navigation requests, and is as follows:
- React Router will call the
clientLoaderfunction on the client side to fetch data for the route. - The result of the
clientLoaderfunction is then used to render the route
loaders
Loaders are functions that are called by React Router to fetch data for your routes on the server side.
They are typically defined in your route modules, and can be used to fetch data from any data source, such as a database or an API.
Here's an example of a loader function fetching a user:
// app/routes/users.ts
export const loader = async ({ params }) => {
const user = await getUser(params.userId);
return user;
};
You can either return the data directly, or you can use the
data utility to return the data along with custom metadata for the Response object:import { data } from 'react-router'
export const loader = async ({ params }) => {
const user = await getUser(params.userId);
return data(user, { status: 200, headers: { 'X-Custom-Header': 'value' } });
};
You can also return complex data structures, such as promises, dates, and more. React Router will serialize and deserialize the data for you automatically using turbo-stream.
export const loader = async ({ params }) => {
// notice no await here, this is a promise and it's intentional
const userPromise = getUser(params.userId);
const date = new Date();
return { user: userPromise, date };
};
Another important thing about loaders is that they can throw responses to indicate errors or redirects.
import { redirect } from 'react-router'
export const loader = async ({ params }) => {
const user = await getUser(params.userId);
if (!user) {
// this will throw a 302 redirect to /login and will stop other loaders from processing and returning data
throw redirect('/login');
}
return user;
};
This is really useful when you want to stop the current flow and redirect the user to another route. As you can see from the example above this is very useful
for handling authentication and authorization in your app.
And finally, you can also throw errors to go into an error boundary:
export const loader = async ({ params }) => {
const user = await getUser(params.userId);
if (!user) {
// this will throw a error and will stop other loaders from processing and returning data
// this will send you into the error boundary that is closest to the current route
throw new Error('User not found');
}
return user;
};
clientLoaders
Client loaders are functions that are called by React Router to fetch data for your routes on the client side.
They are typically defined in your route modules, and can be used to fetch data from any data source, such as a 3rd party API or local storage.
Here's an example of a clientLoader function fetching a user:
// app/routes/users.ts
export const clientLoader = async ({ params }) => {
const user = await fetch(`https://your-api.com/api/users/${params.userId}`).then(res => res.json());
return user;
};
The important thing to know about
clientLoaders is that they act differently depending on if it's a document request or a navigation request and this mostly
comes down to the hydrate property on the clientLoader function.If set to
true & a loader is on the page:- On document requests the clientLoader will be
called after the loaderand will receive the data returned from the loader as an argument. - On navigation requests the clientLoader will be called first and it's in charge of calling the loader if needed.
If set to
false & a loader is on the page:- On document requests the clientLoader will be
skippedand the data returned from the loader will be used to render the route. - On navigation requests the clientLoader will be called first and it's in charge of calling the loader if needed.
If no
loader is on the page:- On both document requests and navigation requests the clientLoader will be called and it's in charge of fetching the data needed for the route.
Example with
hydrate set to true:export const clientLoader = async ({ params, serverLoader }) => {
// serverLoader is the function that calls the loader on the server side and returns the data
const user = await serverLoader();
// you can also fetch more data if needed
return user;
};
// Tells React Router to call this clientLoader on document requests as well
clientLoader.hydrate = true;
Example with
hydrate set to false:export const clientLoader = async ({ params, serverLoader }) => {
// serverLoader is the function that calls the loader on the server side and returns the data
const user = await serverLoader();
// you can also fetch more data if needed
return user;
};
// Tells React Router to NOT call this clientLoader on document requests
// This is the default behavior if hydrate is not set so you can omit this line if you want
clientLoader.hydrate = false;
Also, they work very similarly to loaders in terms of throwing responses and errors to indicate redirects and errors.
Throwing responses:
import { redirect } from 'react-router'
export const clientLoader = async ({ params }) => {
const user = await fetch(`https://your-api.com/api/users/${params.userId}`).then(res => res.json());
if (!user) {
// this will throw a 302 redirect to /login and will stop other clientLoaders/loaders from processing and returning data
throw redirect('/login');
}
return user;
};
Throwing errors:
export const clientLoader = async ({ params }) => {
const user = await fetch(`https://your-api.com/api/users/${params.userId}`).then(res => res.json());
if (!user) {
// this will throw a error and will stop other clientLoaders/loaders from processing and returning data
// this will send you into the error boundary that is closest to the current route
throw new Error('User not found');
}
return user;
};
Summary
Ooof, that was a lot of info to take in, right? But don't worry, you don't have to remember everything at once.
Use this document as a reference whenever you need to, and you'll get the hang of it in no time!
The best way to learn is by doing, so let's get you started with some exercises to practice what you've just heard!
Take a deep breath, and let's continue once you're ready! ๐