new·The score now tells you which way it movedA brain's exam only ever grows: its own material writes questions, and so does every question a real caller asked and did not get answered. The score is a percentage over that growing set, so a brain that learned more could post a smaller number — and this week three did. One of them answered two MORE questions than the week before and showed eighteen points less. Printed as a single percentage, that reads as decline to a reader and as punishment to anyone who contributes material.all news →
mozg.beta
Sign in

React Router · Guides · all subjects

data-loading

87 notes in this subject, read out of this brain and free to use. This is page 1 of 2.

Using fetch in loaders to call backend services

You can use fetch right from your loaders and actions to call your backend API services. This allows the React Router server to act as a proxy between the frontend and your backend systems.

Example: fetch backend data in loader with authorization

```tsx import escapeHtml from "escape-html"; export async function loader() { const apiUrl = "https://api.example.com/some-data.json"; const res = await fetch(apiUrl, { headers: { Authorization: `Bearer ${process.env.API_TOKEN}`, }, }); const data = await res.json(); const prunedData = data.map((record) => { return { id: record.id, title: record.title, formattedBody: escapeHtml(record.content), }; }); return { prunedData }; } ``` This example shows how to use fetch in a loader to call a backend API with authorization headers, and how to prune and transform the response data before returning it to the component.

Mark article as read with useFetcher.submit

This example shows marking an article as read after user engagement. The useFetcher hook calls marker.submit() with userId and an action URL of `/article/${articleId}/mark-as-read`. This triggers a background action without navigating away or changing the URL, suitable for tracking user interactions.

User avatar popup with useFetcher.load

This example demonstrates fetching detailed user data on hover to display in a popup. The UserAvatar component uses useFetcher to load data from `/user-details/${partialUser.id}` when showDetails becomes true. It checks if userDetails.state === 'idle' and userDetails.data exists before rendering the UserPopup. Loading state shows UserPopupLoading while fetching.

Creating a new record with Form and useNavigation

This example demonstrates creating a recipe record. The action validates form data and redirects to the new recipe's page using redirect(`/recipes/${recipe.id}`). The component uses <Form> to submit data, receives validation errors via actionData, and uses useNavigation to track submission state. The navigation.formAction property checks if the current form submission matches the target action path.

Deleting records from a list with useFetcher

When deleting a recipe from a list view, use useFetcher to keep the user on the list. The action receives a form-submitted id and deletes the record from the database. The RecipeListItem component uses useFetcher to submit the delete request, tracks deletion state with fetcher.state !== 'idle', and displays dynamic button text. Each fetcher instance manages its own state independently.

dataStrategy overview and purpose

The dataStrategy option gives you full control over how your action and loader functions are executed. By default, React Router executes all loader functions in parallel for optimal data fetching, but dataStrategy allows you to customize this behavior for advanced use-cases. It lays the foundation for building middleware, context, and caching layers.

DataStrategyMatch fields

A DataStrategyMatch is a normal route match plus additional fields: shouldCallHandler (a function that tells you whether this route's handler should be called for this request), shouldRevalidateArgs (the arguments to be passed to the route's shouldRevalidate for this request), and resolve (a function to handle call through to the route handler allowing custom execution).

DataStrategyResult interface

The dataStrategy function should return a Record<string, DataStrategyResult>. DataStrategyResult is a wrapper object with two fields: type (either 'data' or 'error') and result (the data, Error, Response, or data() wrapper returned by the handler).

Basic dataStrategy example with logging

Example showing how to add logging around handler executions using dataStrategy. The example filters matches using m.shouldCallHandler(), then calls match.resolve() for each match within runClientMiddleware() to execute handlers in parallel and store results in a Record keyed by route.id.

Using runClientMiddleware to wrap handlers

If using middleware on routes, you must leverage the runClientMiddleware helper function to execute middleware around your handlers. Pass a callback to runClientMiddleware that calls match.resolve() for each handler that should execute. runClientMiddleware takes the same arguments as dataStrategy and can be composed with standalone dataStrategy implementations.

Pass custom context to handlers via resolve callback

For fine-grained control over handler execution, pass a callback to match.resolve(). The callback receives the handler function and can call it with custom arguments. Whatever you pass to the handler will be passed as the second parameter to your loader or action function.

Custom revalidation behavior with shouldCallHandler

To alter revalidation behavior, pass your own defaultShouldRevalidate to match.shouldCallHandler(), which passes through to any route level shouldRevalidate functions. The arguments that would be passed to route level shouldRevalidate are available on match.shouldRevalidateArgs.

Migrating from deprecated shouldLoad to shouldCallHandler

The shouldLoad boolean field is deprecated in favor of shouldCallHandler function. The key difference: with shouldLoad, calling resolve() would only call the handler if shouldLoad was true. With shouldCallHandler, you must pre-filter matches before calling resolve(). Only call resolve on the set of matches you wish to run handlers for.

Custom middleware with dataStrategy

You can define custom middleware on routes via the route handle field. Run middleware sequentially to build up context, then run loaders in parallel with the context value. Define middleware as handle.middleware, a function that receives ({ request, params }, context) and can add data to context. Pass context to handlers via the resolve callback.

Custom GraphQL data fetching with dataStrategy

You can implement a custom data fetching strategy without defining individual loaders. Set route.loader=true to mark routes as having a loader, store GraphQL fragments on route.handle, then in dataStrategy collect fragments from matched routes, make a single GraphQL request, and parse results back into individual DataStrategyResult objects keyed by routeId. Never call match.resolve() in this scenario.

dataStrategy is a low-level API for advanced use-cases

The dataStrategy API is low-level and intended for advanced use-cases. It overrides React Router's internal handling of action and loader execution. If implemented incorrectly, it can break your app code. Use with caution and perform appropriate testing.

dataStrategy parameters

A custom dataStrategy function receives the following parameters: matches (an array of DataStrategyMatch instances for routes matched by the current request), request, params, context, runClientMiddleware (a helper function to run middleware for matched routes), and fetcherKey (the fetcher key if this is for a fetcher request).

Fullstack state example code

export async function loader({ request }) { const partialData = await getPartialDataFromDb({ request }); return partialData; } export async function clientLoader({ request, serverLoader }) { const [serverData, clientData] = await Promise.all([ serverLoader(), getClientData(request), ]); return { ...serverData, ...clientData, }; } clientLoader.hydrate = true; export function HydrateFallback() { return <p>Skeleton rendered during SSR</p>; } export default function Component({ loaderData }) { return <>...</>; }

clientLoader does not call on hydration by default

When using a server loader without setting clientLoader.hydrate = true, React Router will not call the clientLoader on hydration. It will only call clientLoader on subsequent navigations.

Skip the server hop pattern with loader and clientLoader

In a Backend-For-Frontend (BFF) architecture, you can bypass the React Router server and communicate directly with a backend API by exporting both a server loader and a clientLoader. Load data from the server loader on document load, then load data from clientLoader on all subsequent navigations. This requires proper authentication handling and assumes no CORS restrictions.

clientLoader.hydrate = true to call clientLoader during initial hydration

Set clientLoader.hydrate = true as a const to instruct React Router to call the clientLoader as part of initial document hydration, even before any navigation occurs.

Fullstack state pattern combining server and client data

To combine data from both server and browser sources: export a server loader to load partial data, export a HydrateFallback component to render during SSR, set clientLoader.hydrate = true to call clientLoader during hydration, then in clientLoader use await Promise.all() to load both serverData via serverLoader() and client data concurrently, and merge them before returning.

Skip the server hop example code

export async function loader({ request }) { const data = await fetchApiFromServer({ request }); return data; } export async function clientLoader({ request }) { const data = await fetchApiFromClient({ request }); return data; }

HydrateFallback component renders when combining server and client data

Export a HydrateFallback component to render during server-side rendering when you need to combine server and client data. This skeleton or placeholder is shown until the clientLoader finishes hydrating with the complete fullstack state.

Server-only data loading with loader

To use server-only data loading, export a loader function. The route will receive server data in the loaderData prop of the component.

Client-only data loading without loader export

To use client-only data loading, export a clientLoader function without exporting a loader. clientLoader.hydrate = true is implied if there is no loader exported. You must also export a HydrateFallback component to render during SSR. The route will receive client data in the loaderData prop of the component.

Client-side caching pattern with hydration primer

Implement client-side caching by: exporting a server loader to load data on document load, setting clientLoader.hydrate = true to prime the cache on hydration, loading subsequent navigations from the cache via clientLoader, and invalidating the cache in clientAction when mutations occur. Do not export a HydrateFallback in this pattern; instead, the route component will be SSR'd and clientLoader runs on hydration, so loader and clientLoader must return the same data initially to avoid hydration errors.

Client-side caching example code

let isInitialRequest = true; export async function clientLoader({ request, serverLoader }) { const cacheKey = generateKey(request); if (isInitialRequest) { isInitialRequest = false; const serverData = await serverLoader(); cache.set(cacheKey, serverData); return serverData; } const cachedData = await cache.get(cacheKey); if (cachedData) { return cachedData; } const serverData = await serverLoader(); cache.set(cacheKey, serverData); return serverData; } clientLoader.hydrate = true; export async function clientAction({ request, serverAction }) { const cacheKey = generateKey(request); cache.delete(cacheKey); const serverData = await serverAction(); return serverData; }

clientLoader and clientAction for browser data handling

clientLoader and clientAction functions are the primary mechanism for data handling when using SPA mode. They allow you to fetch and mutate data directly in the browser.

Accessing URL parameters in loaders

URL parameters can be accessed in loaders and actions via the params object. The property name maps directly to the dynamic segment name. For example, in concerts.$city.tsx, access the city value with params.city.

FileUpload objects should be stored immediately during streaming

FileUpload objects are streaming data from the request body and are not meant to persist for long. You must store them as soon as possible in the uploadHandler, before the request completes. After storing, you can return a LazyFile that accesses the file's content only when needed.

File uploads require multipart/form-data enctype

To handle file uploads in a form, you must set the form's enctype attribute to 'multipart/form-data'. Without this, file uploads will not work.

Install @remix-run/form-data-parser for file upload handling

The @remix-run/form-data-parser package provides a wrapper around request.formData() that adds streaming support for handling file uploads. Install it with: npm i @remix-run/form-data-parser

parseFormData function takes an uploadHandler callback

The parseFormData function accepts an uploadHandler function as an argument. This handler is called for each file upload in the form and receives a FileUpload object containing details about the file being uploaded.

Install @remix-run/file-storage for local file storage

The @remix-run/file-storage package provides a key/value interface for storing File objects on the server, similar to how localStorage works in the browser. Install it with: npm i @remix-run/file-storage

LocalFileStorage configuration for avatar uploads

Create a LocalFileStorage instance by importing LocalFileStorage from '@remix-run/file-storage/local' and passing a directory path. Example: const fileStorage = new LocalFileStorage('./uploads/avatars'). This instance can then be used across multiple routes to store and retrieve uploaded files.

Basic file upload action handler example

Example showing how to set up a file upload handler in a route action: ```tsx import { type FileUpload, parseFormData, } from "@remix-run/form-data-parser"; import type { Route } from "./+types/user-profile"; export async function action({ request, }: Route.ActionArgs) { const uploadHandler = async (fileUpload: FileUpload) => { if (fileUpload.fieldName === "avatar") { // process the upload and return a File } }; const formData = await parseFormData( request, uploadHandler, ); const file = formData.get("avatar"); } ``` This pattern allows processing of uploaded files with the uploadHandler callback, and the form data is available after parseFormData completes.

Complete file upload with local storage example

Example of storing uploaded files using LocalFileStorage: ```tsx import { type FileUpload, parseFormData, } from "@remix-run/form-data-parser"; import { fileStorage, getStorageKey, } from "~/avatar-storage.server"; import type { Route } from "./+types/user-profile"; export async function action({ request, params, }: Route.ActionArgs) { async function uploadHandler(fileUpload: FileUpload) { if ( fileUpload.fieldName === "avatar" && fileUpload.type.startsWith("image/") ) { let storageKey = getStorageKey(params.id); await fileStorage.set(storageKey, fileUpload); return fileStorage.get(storageKey); } } const formData = await parseFormData( request, uploadHandler, ); } ``` This example shows checking the file type, storing the file with a unique key, and returning a LazyFile for later access.

Serve uploaded files with a resource route

Example of creating a loader route to serve stored files: ```tsx import { fileStorage, getStorageKey, } from "~/avatar-storage.server"; import type { Route } from "./+types/avatar"; export async function loader({ params }: Route.LoaderArgs) { const storageKey = getStorageKey(params.id); const file = await fileStorage.get(storageKey); if (!file) { throw new Response("User avatar not found", { status: 404, }); } return new Response(file.stream(), { headers: { "Content-Type": file.type, "Content-Disposition": `attachment; filename=${file.name}`, }, }); } ``` This resource route retrieves a stored file, handles the 404 case when the file doesn't exist, and returns it with appropriate Content-Type and Content-Disposition headers.

FileUpload object has fieldName and type properties

A FileUpload object includes at least fieldName (the form field name) and type (the MIME type) properties. You can check fieldName to identify which field is being uploaded and check type.startsWith('image/') to validate the file type.

LazyFile defers reading until content is accessed

A LazyFile is a File-like object returned by fileStorage.get() that waits to read the file's content until it is actually requested (for example, when calling file.stream()). This allows you to pass file references through your application without loading the entire file into memory immediately.

Action function receives FormData from POST request

In the action handler, retrieve form data using await request.formData(). Each form field name becomes a key accessible via formData.get('fieldName'). Convert values to strings as needed.

Return validation errors with 400 status code

When validation fails, return data using data({ errors }, { status: 400 }). The 400 status signals a client validation error (Bad Request). Only 2xx status codes trigger page data revalidation in React Router, so a 400 status prevents unwanted revalidation after an action.

Form validation flow with useFetcher example

Example showing a signup form with email and password validation: export default function Signup(_: Route.ComponentProps) { let fetcher = useFetcher(); let errors = fetcher.data?.errors; return ( <fetcher.Form method="post"> <p> <input type="email" name="email" /> {errors?.email ? <em>{errors.email}</em> : null} </p> <p> <input type="password" name="password" /> {errors?.password ? ( <em>{errors.password}</em> ) : null} </p> <button type="submit">Sign Up</button> </fetcher.Form> ); } export async function action({ request, }: Route.ActionArgs) { const formData = await request.formData(); const email = String(formData.get("email")); const password = String(formData.get("password")); const errors = {}; if (!email.includes("@")) { errors.email = "Invalid email address"; } if (password.length < 12) { errors.password = "Password should be at least 12 characters"; } if (Object.keys(errors).length > 0) { return data({ errors }, { status: 400 }); } return redirect("/dashboard"); }

Form validation with useFetcher

Use the useFetcher hook to submit forms and handle validation errors. Create a fetcher.Form with method="post" to submit data without navigation, then access validation errors via fetcher.data?.errors to display them to the user.

Set-Cookie headers automatic preservation in nested routes

Set-Cookie headers are automatically preserved from headers, loader, and action in parent routes, even without exporting headers from the child route. This is a notable exception to the requirement to explicitly return headers from the child route.

Header merging in nested routes

In nested routes, the headers from the deepest matching route will be sent by default. To keep both parent and child headers, you must merge them in the child route using the parentHeaders parameter received in HeadersArgs.

Appending headers to parent headers

Use parentHeaders.append() in the child route headers export to add a header without overwriting what the parent may have set. Example: parentHeaders.append('Permissions-Policy', 'geolocation=()')

Overwriting parent headers with set method

Use parentHeaders.set() instead of append() to overwrite a parent header. Example: parentHeaders.set('Cache-Control', 'max-age=3600, s-maxage=86400')

Strategy to avoid header merging in nested routes

Only define headers in leaf routes (index routes and child routes without children) and not in parent routes. This avoids the need to merge headers in child routes.

Setting global headers in entry.server.tsx

The handleRequest export in entry.server.tsx receives responseHeaders as an argument. You can use responseHeaders.set() or responseHeaders.append() to add global headers. The responseHeaders are passed to the Response constructor. If entry.server.tsx does not exist, run 'react-router reveal' to create it.

Example: Reading and using request headers in loader

export async function loader({ request }: Route.LoaderArgs) { const userAgent = request.headers.get('User-Agent'); const hasCookies = request.headers.has('Cookie'); // ... }

Example: Setting response headers from route module

import { Route } from './+types/some-route'; export function headers(_: Route.HeadersArgs) { return { 'Content-Security-Policy': "default-src 'self'", 'X-Frame-Options': 'DENY', 'X-Content-Type-Options': 'nosniff', 'Cache-Control': 'max-age=3600, s-maxage=86400', }; }

Example: Setting headers from loader with data function

import { data } from 'react-router'; export async function loader({ params }: LoaderArgs) { let [page, ms] = await fakeTimeCall(await getPage(params.id)); return data(page, { headers: { 'Server-Timing': `page;dur=${ms};desc="Page query"`, }, }); }

Example: Returning loader or action headers from headers export

function hasAnyHeaders(headers: Headers): boolean { return [...headers].length > 0; } export function headers({ actionHeaders, loaderHeaders }: HeadersArgs) { return hasAnyHeaders(actionHeaders) ? actionHeaders : loaderHeaders; }

Example: Setting global headers in entry.server.tsx

export default async function handleRequest( request, responseStatusCode, responseHeaders, routerContext, loadContext, ) { responseHeaders.set('X-App-Version', routerContext.manifest.version); return new Response(await getStream(), { headers: responseHeaders, status: responseStatusCode, }); }

Setting response headers from route modules

Response headers are primarily defined with the route module headers export. The function receives Route.HeadersArgs and can return either a Headers instance or HeadersInit object. Example security headers: Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, and Cache-Control.

Reading request headers in loaders

The request sent to route handlers is a standard Web Fetch Request, so you can read headers directly from the request.headers property. Standard Headers methods are available, such as request.headers.get('User-Agent') to retrieve a specific header value and request.headers.has('Cookie') to check if a header exists.

Setting headers from loaders and actions

To set headers dependent on loader data, wrap the return value in data() and pass headers in the second argument. The data function comes from react-router. Example: return data(page, { headers: { 'Server-Timing': `page;dur=${ms};desc="Page query"` } }).

Give your agent this brain