Loaders are useful for one-time loading of data from the server to the client. They can be used in both page routes and layout files in your app directory.
Loaders run on the server based on their render mode - during build-time for SSG routes, or on each request for SPA or SSR routes. Layout loaders and page loaders run in parallel for optimal performance.
Loaders work similarly on native platforms as they do on web:
This means native apps can use the same loader patterns as web apps, with data fetching behavior determined by the route’s render mode rather than the platform.
Loaders and their imports are removed from client bundles, so you can access private information from within the loader. The data returned from the loader will be passed to the client, and so should be clear of private information.
There are two hooks available for accessing loader data:
The basic hook for accessing loader data:
For advanced use cases requiring manual refetch or loading states:
Both hooks are automatically type safe. See useLoader and useLoaderState for detailed documentation.
To access loader data from other routes (like a layout accessing page data, or vice versa), use useMatches:
See useMatches for detailed documentation.
Layouts can export a loader function just like pages:
app/docs/_layout.tsx
See Layout Loaders for more details.
Loaders receive a single argument object:
The params key will provide values from any dynamic route segments:
app/user/[id].tsx
The path key is the fully resolved pathname:
app/user/[id].tsx
Only for ssr type routes. This will pass the same Web standard Request object as API routes.
Most JavaScript values, including primitives and objects, will be converted to JSON.
You may also return a Response:
A handy pattern for loaders is throwing a response to end it early:
Use the redirect() helper in loaders to protect SSR routes. This is the recommended approach for server-side auth guards — it prevents unauthorized content from ever reaching the client.
app/dashboard+ssr.tsx
Both throw redirect() and return redirect() work. throw is often cleaner since it stops execution immediately and makes it obvious the remaining code won’t run.
During a direct page load, redirect() returns a standard HTTP 302 response — the browser redirects normally.
During client-side <Link> navigation, loaders run on the server and the client fetches the result as a JavaScript module. Without special handling, the browser’s module loader would silently follow a 302, try to parse the HTML login page as JavaScript, and fail. One handles this differently:
REPLACE action — the protected page never appears in the historyThis means no sensitive data reaches the client when a redirect occurs. The server only sends back the redirect path, not the page content or loader data.
For SSG routes, if a loader redirects during the build (e.g., no authenticated request is available), One generates a static redirect file. In production with a runtime server, the server re-evaluates the loader on each request, so authenticated users see the real page while unauthenticated users get redirected.
Use +ssr routes for protected pages so loaders run on every request. SSG routes evaluate loaders once at build time, which won’t have access to per-request auth state.
See the Loader Redirects guide for a complete walkthrough and the redirect helper for the API reference.
Use setResponseHeaders to set HTTP headers on the response from within your loader. This is useful for caching, cookies, and custom headers.
Set cache headers to enable Incremental Static Regeneration - serve SSR pages with static-like performance:
Set cookies by appending Set-Cookie headers:
Read cookies from the request (SSR routes only):
Add any custom headers:
See setResponseHeaders for more details.
Routes can export a loaderCache function to deduplicate concurrent SSR loader calls. When multiple requests hit the same route at the same time, only one loader execution runs and the result is shared across all waiting requests.
Return a string key from loaderCache to coalesce concurrent requests with matching keys:
app/cards/[id]+ssr.tsx
Without ttl, only truly concurrent requests are coalesced — the cache entry is cleaned up immediately after the loader resolves.
Return an object with key and ttl to cache the result for a time window (in milliseconds):
With ttl, subsequent requests within the TTL window reuse the cached result without running the loader again.
loaderCache receives (params, request) so you can include auth headers or other request data in the key:
loaderCache improved throughput from ~570 req/sec to ~2,000 req/sec (3.5x)During development, you can use watchFile to register file dependencies in your loader. When these files change, the loader data will automatically refresh without a full page reload.
This is useful for content-driven sites where loaders read from MDX files, JSON, or other data files:
watchFile is a no-op in production and on the client, so it’s safe to use unconditionally. The path should be the same path you pass to fs.readFile or similar functions.
The @vxrn/mdx package has built-in HMR support. When used in a One loader, MDX files will automatically trigger hot reload when changed:
If you’re building a library that reads files and want to support One’s loader HMR, you can use the global hook pattern. This allows your library to work with One’s HMR without depending on the one package:
When One is present, it registers the watchFile implementation on the global object. Your library checks for it and calls it if available - no coupling required. The @vxrn/mdx package exports notifyFileRead if you prefer to import it directly.
One provides two ways to validate routes before navigation: validateParams for schema-based validation and validateRoute for async validation logic.
Export a schema to validate route params before navigation. Works with Zod, Valibot, or custom functions:
If validation fails, navigation is blocked and an error is shown in the Error Panel (Alt+E).
For async validation (like checking if a resource exists), export a validateRoute function:
Both can validate routes, but they serve different purposes:
| Feature | validateParams | validateRoute |
|---|---|---|
| Runs on | Client | Client |
| Use case | Schema validation | Async checks |
Use validateParams for simple schema validation (UUIDs, formats, types).
Use validateRoute for async checks on the client (API calls, existence checks).
For server-side auth and redirects, use your loader function and throw a redirect response.
Access validation state from any component:
Edit this page on GitHub.