routeLoader$()
Route Loaders load data in the server so it becomes available to use inside Qwik Components. They trigger when SPA/MPA navigation happens so they can be invoked by Qwik Components during rendering.
Route Loaders can only be declared inside the src/routes
folder, in a layout.tsx
or index.tsx
file, and they MUST be exported.
If you want to manage common reusable routeLoaders$ it is essential that this function is re-exported from within 'layout.tsx' or 'index.tsx file of the existing route otherwise it will not run or throw exception. For more information check the cookbook.
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
export const useProductDetails = routeLoader$(async (requestEvent) => {
// This code runs only on the server, after every navigation
const res = await fetch(`https://.../products/${requestEvent.params.productId}`);
const product = await res.json();
return product as Product;
});
export default component$(() => {
// In order to access the `routeLoader$` data within a Qwik Component, you need to call the hook.
const signal = useProductDetails(); // Readonly<Signal<Product>>
return <p>Product name: {signal.value.product.name}</p>;
});
Route Loaders are perfect to fetch data from a database or an API. For example you can use them to fetch data from a CMS, a weather API, or a list of users from your database.
You should not use a
routeLoader$
to create a REST API, for that youโd be better off using an Endpoint, which allows you to have tight control over the response headers and body.
routeLoader$
s
Multiple Multiple routeLoader$
s are allowed across the whole application, and they can be used in any Qwik Component. You can even declare multiple routeLoader$
s in the same file.
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
import { Footer } from '../components/footer.tsx';
export const useProductData = routeLoader$(async () => {
const res = await fetch('https://.../product');
const product = (await res.json()) as Product;
return product;
});
export default component$(() => {
const signal = useProductData();
return (
<main>
<Slot />
<Footer />
</main>
);
});
import { component$ } from '@builder.io/qwik';
// Import the loader from the layout
import { useProductData } from '../routes/layout.tsx';
export const Footer = component$(() => {
// Consume the loader data
const signal = useProductData();
return <footer>Product name: {signal.value.product.name}</footer>;
});
The above example shows using useProductData()
in two different components across different files. This is intentional behavior.
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
export const useLoginStatus = routeLoader$(async ({ cookie }) => {
return {
isUserLoggedIn: checkCookie(cookie),
};
});
export const useCurrentUser = routeLoader$(async ({ cookie }) => {
return {
user: currentUserFromCookie(cookie),
};
});
export default component$(() => {
const loginStatus = useLoginStatus();
const currentUser = useCurrentUser();
return (
<section>
<h1>Admin</h1>
{loginStatus.value.isUserLoggedIn ? (
<p>Welcome {currentUser.value.user.name}</p>
) : (
<p>You are not logged in</p>
)}
</section>
);
});
The above example shows two routeLoader$
s being used in the same file. A generic useLoginStatus
loader is used to check if the user is logged in, and a more specific useCurrentUser
loader is used to retrieve the user data.
RequestEvent
Just like middleware or endpoint onRequest
and onGet
, routeLoader$
s have access to the RequestEvent
API which includes information about the current HTTP request.
This information comes in handy when the loader needs to conditionally return data based on the request, or it needs to override the response status, headers, or body manually.
import { routeLoader$ } from '@builder.io/qwik-city';
export const useProductRecommendations = routeLoader$(async (requestEvent) => {
console.log('Request headers:', requestEvent.request.headers);
console.log('Request cookies:', requestEvent.cookie);
console.log('Request url:', requestEvent.url);
console.log('Request method:', requestEvent.method);
console.log('Request params:', requestEvent.params);
// Use request details to fetch personalized data
const res = fetch(`https://.../recommendations?user=${requestEvent.params.user}`);
const recommendedProducts = (await res.json()) as Product[];
return recommendedProducts;
});
routeLoader$
data within another routeLoader$
Access the You can access the data from one routeLoader$
inside another routeLoader$
using the requestEvent.resolveValue
method.
import { routeLoader$ } from '@builder.io/qwik-city';
export const useProductDetails = routeLoader$(async (requestEvent) => {
const res = await fetch(`https://.../products/${requestEvent.params.productId}`);
const product = await res.json();
return product;
});
export const useProductRecommendations = routeLoader$(async (requestEvent) => {
// Resolve the product details from the other loader
const product = await requestEvent.resolveValue(useProductDetails);
// Use the product details to fetch personalized data
const res = fetch(`https://.../recommendations?product=${product.id}`);
const recommendedProducts = (await res.json()) as Product[];
return recommendedProducts;
});
The same API can be used to access the data from a
routeAction$
or aglobalAction$
.
routeLoader$
Failed values with routeLoader$
s can use the fail
method to return a failed value, which is a special value that indicates that the loader didn't succeed loading the expected data.
In addition, the fail
function allows routeLoader$
to override the HTTP status code, for example retuning 404.
This is useful when the loader needs to return an "error" value that is not undefined
, but it also needs to indicate that the data failed to load.
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
export const useProductDetails = routeLoader$(async (requestEvent) => {
const product = await db.from('products').filter('id', 'eq', requestEvent.params.productId);
if (!product) {
// Return a failed value to indicate that product was not found
return requestEvent.fail(404, {
errorMessage: 'Product not found'
});
}
return {
productName: product.name
};
});
export default component$(() => {
const product = useProductDetails();
if (product.value.errorMessage) {
// Render UI for failed value
return <div>{product.value.errorMessage}</div>;
}
return <div>Product name: {product.value.productName}</div>;
});
Handling Relative URLs in Loaders
In the server-side execution environment, it's crucial to convert relative URLs to absolute URLs for proper functionality. This can be achieved by prefixing the relative URL with the origin
from the useLocation()
function.
import { component$ } from '@builder.io/qwik';
import { useLocation } from '@builder.io/qwik-city';
export default component$(() => {
const location = useLocation();
const relativeUrl = '/mock-data';
const absoluteUrl = location.url.origin + relativeUrl;
return (
<section>
<div>Relative URL: {relativeUrl}</div>
<div>Absolute URL: {absoluteUrl}</div>
</section>
);
});
Performance considerations
Route Loaders are executed on the server, after every navigation. This means that they are executed every time a user navigates to a page in an SPA or MPA, and they are executed even if the user is navigating to the same page.
Loaders execute after the Qwik Middleware handlers (onRequest
, onGet
, onPost
, etc), and before the Qwik Components are rendered. This allows the loaders to start fetching data as soon as possible, reducing latency.