Documentation Index
Fetch the complete documentation index at: https://mintlify.com/remix-run/react-router/llms.txt
Use this file to discover all available pages before exploring further.
Custom Data Strategies
The dataStrategy API allows you to customize how React Router executes loaders and actions, giving you control over when and how data is fetched.
Overview
By default, React Router calls loaders in parallel and actions individually. The dataStrategy function lets you override this behavior to implement patterns like:
- Sequential loader execution
- Single fetch requests for all loaders (like Remix’s Single Fetch)
- Middleware patterns with context passing
- Custom error handling and response decoding
Basic Usage
import { createBrowserRouter } from "react-router";
const router = createBrowserRouter(routes, {
dataStrategy({ matches }) {
// Default behavior: call all loaders in parallel
return Promise.all(
matches.map((match) => match.resolve())
);
},
});
The dataStrategy Function
The dataStrategy receives a DataStrategyFunctionArgs object:
interface DataStrategyFunctionArgs {
request: Request;
params: Params;
context?: unknown;
matches: DataStrategyMatch[];
}
Matches and resolve()
Each match in the matches array has a resolve() method that:
- Waits for
route.lazy to load if needed
- Determines whether to call the loader or action
- Handles
shouldRevalidate logic internally
- Returns a
HandlerResult with the data or error
interface HandlerResult {
type: 'data' | 'error';
result: unknown;
status?: number;
}
Sequential Loaders
Execute loaders one at a time, passing context between them:
async function dataStrategy({ matches }) {
let context = {};
let results = [];
for (let match of matches) {
let result = await match.resolve((handler) => {
// Handler receives context as second argument
return handler(context);
});
results.push(result);
// Update context for next loader
if (result.type === 'data') {
context = { ...context, ...result.result };
}
}
return results;
}
Middleware Pattern
Implement middleware that runs before loaders:
async function dataStrategy({ matches }) {
// Run middlewares sequentially
let context = {};
for (let match of matches) {
if (match.route.handle?.middleware) {
await match.route.handle.middleware(context);
}
}
// Run loaders in parallel with context
return Promise.all(
matches.map((match) =>
match.resolve((handler) => handler(context))
)
);
}
Define middleware in your routes:
const routes = [
{
path: "/",
handle: {
async middleware(context) {
context.user = await getUser();
},
},
loader({ request }, context) {
// Access context.user
return { data: context.user };
},
},
];
Single Fetch Pattern
Make one request for all loaders:
async function dataStrategy({ matches, request }) {
// Build a single fetch request
const url = new URL(request.url);
url.searchParams.set(
'routes',
matches.map(m => m.route.id).join(',')
);
const response = await fetch(url);
const allData = await response.json();
// Map data back to routes
return matches.map((match) =>
match.resolve(() => ({
type: 'data',
result: allData[match.route.id],
}))
);
}
Custom Response Decoding
Decode responses using custom formats:
import { decode } from 'turbo-stream';
async function dataStrategy({ matches }) {
return Promise.all(
matches.map(async (match) =>
match.resolve(async (handler) => {
const response = await handler();
if (response instanceof Response) {
return {
type: 'data',
result: await decode(response.body),
};
}
return response;
})
)
);
}
Error Handling
The resolve() method never throws. Errors are returned in the result:
const result = await match.resolve();
if (result.type === 'error') {
console.error('Loader failed:', result.result);
// Log to error tracking service
}
Working with Actions and Fetchers
The dataStrategy handles actions and fetchers too. For single-match operations, you’ll receive a single-item array:
async function dataStrategy({ matches }) {
// matches.length === 1 for actions and fetchers
// matches.length > 1 for navigation loaders
return Promise.all(
matches.map((match) => match.resolve())
);
}
Framework Mode
In framework mode, configure dataStrategy via createRequestHandler:
import { createRequestHandler } from "@react-router/express";
app.all(
"*",
createRequestHandler({
build: await import("./build/server"),
dataStrategy: async ({ matches }) => {
// Custom strategy
},
})
);
Best Practices
- Don’t modify request/params: The handler receives its own arguments
- Use resolve() callbacks sparingly: Only when you need custom logic
- Handle shouldRevalidate: It’s already handled by
resolve()
- Consider performance: Sequential loading can slow down your app
- Test both states: With and without interruptions