Article summary
TanStack Router’s beforeLoad and loader functions allow for tight control over what happens while a route, well… loads! At first glance, it isn’t super clear when to use which. Both run before the route component renders, so why have two separate functions?
beforeLoad
When a browser navigates to a URL that matches a route, two steps take place before a component is rendered: pre-loading, and then loading. beforeLoad handles the pre-loading step.
During pre-loading, the beforeLoad function handles any necessary checks before the route begins loading. In practice, it’s essentially the middleware for the route. Importantly, pre-loading occurs not just for the matched route, but for its parent routes as well.
Example
Say we have a route that looks like this:
/dashboard/admin
The route consists of the /dashboard route as well as the /admin route, which is a child of the /dashboard route. When this URL is matched, we begin sequentially pre-loading each one, starting from the parent route.
Here’s what our dashboard route definition looks like:
export const Route = createFileRoute("/dashboard")({
beforeLoad: async () => {
const user = await getUser();
if (!user) {
throw redirect({ to: "/login" });
}
return { user };
},
Here, we’re doing a quick check to see if a user is logged in. If there is no user found, the user must not be logged in. So, we redirect back to the login page. Pre-loading for a route stops once we throw an exception (or a redirect in this case). Because pre-loading occurs sequentially, the beforeLoad for the /admin route would never be triggered.
If a user exists, then we return the user. This appends the user to the router context for the next beforeLoad the use.
Admin Route Definition
Here’s what our admin route definition looks like:
export const Route = createFileRoute("/dashboard/admin")({
beforeLoad: async ({ context }) => {
const user = context.user;
if (!user.isAdmin) {
throw redirect({ to: "/dashboard" });
}
},
Once the beforeLoad for the /dashboard route finishes, the beforeLoad for the /admin route is called. Here, we grab the user off of the context that our /dashboard beforeLoad provided. We want to do an auth check to ensure that the user can access this route. If they can’t, we redirect to the /dashboard. If they can, then the pre-loading for the route successfully completes.
To recap, the important things to know about beforeLoad functions are:
- They run sequentially, one at a time. A parent’s beforeLoad must complete before any child’s beforeLoad functions run.
- If an error or a redirect are thrown in a beforeLoad, loading will not take place for the route nor its child routes.
- By returning data, the route context that’s used by all of the child routes can be added on to.
The pre-loading process for the route has finished, which means we can now begin loading.
loader
The loading step is where any data that the route component needs can be fetched. It has access to the context that was built in the beforeLoad functions, so we can use that here as well.
Dashboard Route
Here’s the loader for our /dashboard route:
export const Route = createFileRoute("/dashboard")({
beforeLoad: async () => . . .
loader: async ({ context }) => {
const user = context.user;
const dashboardDataForUser = await getData(user.id);
return { user, dashboardDataForUser };
},
component: Dashboard,
});
Our dashboard component needs the user, along with some personalized data for the user. The loader is where we grab that data and send it to the component.
Admin Route
Here’s the loader for the /admin route:
export const Route = createFileRoute("/dashboard/admin")({
beforeLoad: async () => . . .
loader: async () => {
const adminData = await getAdminData();
return { adminData };
},
component: Admin,
});
Same situation. We just want to fetch some data for use by our component, so we fetch it here in the loader and then return it.
Unlike pre-loading, loading happens in parallel for all routes. Once the pre-loading for our /dashboard/admin route completes, the loader functions for both the /dashboard and the /admin routes run at the same time.
Once loading completes, the route component is rendered and has access to the data provided by the loader.
Now, in our admin component, we can access the data we need. Our component needs the user that was returned in the /dashboard route loader, and the admin data from the /admin route loader. From the component, we can access the data using the useLoaderData hook like this:
function Admin() {
const { user } = useLoaderData({ from: "/dashboard" });
const { adminData } = useLoaderData();
Note
Note that when we’re using data returned by a parent route’s loader and not the loader of the current route itself, we need to specify the route in the useLoaderData call.
Important things to know about loader functions:
- They run in parallel, allowing for faster data fetching.
- Data returned from them is accessible in the route components using the useLoaderData() hook.
- They include built-in caching to further speed up route loading.
beforeLoad vs. loader
Ultimately, the major difference between the two comes down to their intended purposes. beforeLoad is for checking if a route should be loaded and building context, and loader is for fetching data to be used in components.
If you’re still unsure of where in the process some operation needs take place, ask yourself the following questions when building out your route:
Do I need to check if a route can be accessed?
Use beforeLoad
Do I need to provide data for use in a child route’s beforeLoad or a loader?
Use beforeLoad
Do I need to provide data for use in a component?
Use loader
Happy routing!