UNPKG

13.3 kB Markdown View Raw
1---
2title: Data Strategy
3---
4
5# Data Strategy
6
7[MODES: data]
8
9<docs-warning>This is a low-level API intended for advanced use-cases. This overrides React Router's internal handling of `action`/`loader` execution, and if done incorrectly will break your app code. Please use with caution and perform the appropriate testing.</docs-warning>
10
11## Overview
12
13By default, React Router is opinionated about how your data is loaded/submitted - and most notably, executes all of your [`loader`][loader] functions in parallel for optimal data fetching. While we think this is the right behavior for most use-cases, we realize that there is no "one size fits all" solution when it comes to data fetching for the wide landscape of application requirements.
14
15The [`dataStrategy`][data-strategy] option gives you full control over how your [`action`][action]/[`loader`][loader] functions are executed and lays the foundation to build in more advanced APIs such as middleware, context, and caching layers. Over time, we expect that we'll leverage this API internally to bring more first class APIs to React Router, but until then (and beyond), this is your way to add more advanced functionality for your application's data needs.
16
17## Usage
18
19A custom `dataStrategy` receives the `loader`/`action` arguments (`request`, `params`, `context`) plus a few more that allow you to decide how you want to control the executions for your application:
20
21- `matches`: An array of `DataStrategyMatch` instances for the routes matched by the current `request`
22- `runClientMiddleware`: A helper function to run the middleware for the matched routes
23- `fetcherKey`: The key of the fetcher targeted by this execution, or `null` for route loader/action executions. Fetchers reloaded by a navigation or explicit revalidation also provide their key.
24- `unstable_initiator`: The operation that initiated this execution (see [Request Metadata](#request-metadata-unstable))
25- `unstable_navigationType`: The history action for a navigation-initiated execution, or `null` for other executions
26
27A `DataStrategyMatch` is a normal route match plus a few additional fields:
28
29- `shouldCallHandler`: A function that tells you whether this routes handler should be called for this request
30- `shouldRevalidateArgs`: The arguments that to be passed to the routes `shouldRevalidate` for this request
31- ~~`shouldLoad`~~: A boolean field for whether this routes handler should be run for this request
32 - Deprecated in favor of the more powerful `shouldCallHandler` API
33- `resolve`: A function to handle call through to the route handler, and also allow you custom execution of the handler
34
35Here's a basic example that adds logging around the handler executions:
36
37```tsx
38let router = createBrowserRouter(routes, {
39 async dataStrategy({
40 matches,
41 request,
42 runClientMiddleware,
43 }) {
44 // Determine which matches are expected to be executed for this request.
45 // - For loading navigations, this will return true for new routes + existing
46 // routes requiring revalidation
47 // - For submission navigations, this will only return true for the action route
48 // - For fetcher calls, this will only return true for the fetcher route
49 const matchesToLoad = matches.filter((m) =>
50 m.shouldCallHandler(),
51 );
52
53 // For each match that we want to execute, call match.resolve() to execute
54 // the handler and store the result
55 const results: Record<string, DataStrategyResult> = {};
56 await runClientMiddleware(() =>
57 Promise.all(
58 matchesToLoad.map(async (match) => {
59 console.log(`Processing ${match.route.id}`);
60 // The resolve function calls through to the route handler
61 results[match.route.id] = await match.resolve();
62 }),
63 ),
64 );
65 return results;
66 },
67});
68```
69
70The `dataStrategy` function should return a `Record<string, DataStrategyResult>` which contains the result for each handler that was executed. A `DataStrategyResult` is just a wrapper object that indicates if the handler returned or threw:
71
72```ts
73interface DataStrategyResult {
74 type: "data" | "error";
75 result: unknown; // data, Error, Response, data()
76}
77```
78
79### Request Metadata (unstable)
80
81<docs-warning>`unstable_initiator` and `unstable_navigationType` are experimental and may change in minor or patch releases.</docs-warning>
82
83`unstable_initiator` identifies why the data strategy is running:
84
85- `"initialization"`: Initial router data loading
86- `"navigation"`: A navigation, including action submissions and subsequent loader revalidation
87- `"fetcher"`: A fetcher operation, including loader revalidation after a fetcher action
88- `"revalidation"`: Explicit revalidation, such as `useRevalidator().revalidate()`
89- `"static"`: Execution by `createStaticHandler` on the server
90
91Requests that reload fetchers during a navigation or explicit revalidation retain that initiator while providing the targeted `fetcherKey`. Don't use `fetcherKey` alone to determine what initiated the execution.
92
93`unstable_navigationType` is `"PUSH"`, `"REPLACE"`, or `"POP"` for navigation-initiated executions, including fetchers reloaded by that navigation. It is `null` for initialization, standalone fetcher operations, explicit revalidation, and static handler executions. It describes the in-flight navigation rather than the previously committed location.
94
95You can use this metadata to choose a loading or caching policy. For example, a Data Mode `dataStrategy` can distinguish back/forward navigations with `unstable_initiator === "navigation" && unstable_navigationType === "POP"`. Your strategy remains responsible for executing the appropriate handlers and returning their results.
96
97Framework Mode exposes equivalent request metadata through [`unstable_fetch`](./custom-fetch#request-context), which customizes server requests without replacing loader/action execution.
98
99### Calling Route Middleware
100
101If you are using `middleware` on your routes, you need to leverage the `callClientMiddleware` helper function to execute `middleware` around your handlers:
102
103```tsx
104let router = createBrowserRouter(routes, {
105 async dataStrategy({
106 matches,
107 request,
108 runClientMiddleware,
109 }) {
110 const matchesToLoad = matches.filter((m) =>
111 m.shouldCallHandler(),
112 );
113 const results: Record<string, DataStrategyResult> = {};
114
115 // Run middleware and execute handlers at the end of the middleware chain
116 await runClientMiddleware(() =>
117 Promise.all(
118 matchesToLoad.map(async (match) => {
119 results[match.route.id] = await match.resolve();
120 }),
121 ),
122 );
123 return results;
124 },
125});
126```
127
128`runClientMiddleware` takes the same arguments as `dataStrategy` so it can also be easily composed with a standalone `dataStrategy` implementation:
129
130```tsx
131const loggingDataStrategy: DataStrategyFunction = () => {
132 /* ... */
133};
134
135let router = createBrowserRouter(routes, {
136 async dataStrategy({ runClientMiddleware }) {
137 let results = await runClientMiddleware(
138 loggingDataStrategy,
139 );
140 return results;
141 },
142});
143```
144
145### Advanced handler execution
146
147If you want more fine-grained control over the execution of the handler, you can pass a callback to `match.resolve()`:
148
149```tsx
150// Assume a loader shape such as
151function loader({ request }, customContext) {...}
152
153// In your dataStrategy, you can pass this context from inside a resolve callback
154await Promise.all(
155 matchesToLoad.map((match, i) =>
156 match.resolve((handler) => {
157 let customContext = getCustomContext();
158 // Call the handler and p[ass a custom parameter as the handler's second argument
159 return handler(customContext);
160 }),
161 ),
162);
163```
164
165### Custom Revalidation Behavior
166
167If you want to alter the revalidation behavior, you can pass your own `defaultShouldRevalidate` to `match.shouldCallHandler()` which will pass through to any route level `shouldRevalidate` functions. The arguments that would be passed to the route level `shouldRevalidate` are available on `match.shouldRevalidateArgs`:
168
169```tsx
170const matchesToLoad = matches.filter((match) => {
171 let defaultShouldRevalidate = customShouldRevalidate(
172 match.shouldRevalidateArgs,
173 );
174 return m.shouldCallHandler(defaultShouldRevalidate);
175});
176```
177
178## Migrating away from `shouldLoad`
179
180Now that we have stabilized the new `match.shouldCallHandler()`/`match.shouldRevalidateArgs` fields, it's recommended to move away from the now-deprecated `match.shouldLoad` API. The prior boolean approach did not allow for custom `dataStrategy` functions to alter the default revalidation behavior, so the new function-based APIs were created to allow that.
181
182The major difference between these two APIs is that when using `shouldLoad`, calling `resolve()` would _only_ call the handler if `shouldLoad` was `true`. You could safely call it for all matches even if only a subset needed to have their handlers executed.
183
184With `shouldCallHandler`, you are in charge of which handlers should be called so calling resolve will automatically call the handler. You should only call resolve on the set of matches you wish to run handlers for.
185
186Here's an example change from the prior API to the new API. Note that we pre-filter the `matchesToLoad` before calling `resolve()`:
187
188```tsx diff
189let results = {};
190+let matchesToLoad = matches.filter(m => m.shouldCallHandler());
191await Promise.all(() =>
192- matches.map((m) => {
193+ matchesToLoad.map((m) => {
194 results[m.route.id] = await m.resolve();
195 }),
196);
197return results;
198```
199
200## Advanced Use Cases
201
202### Custom Middleware
203
204<docs-info>This is an unlikely use-case now that React Router has built-in middleware, but if you wish to use a custom middleware you can do so with a `dataStrategy`.</docs-info>
205
206Let's define a middleware on each route via [`handle`](../start/data/route-object#handle)
207and call middleware sequentially first, then call all
208[`loader`](../start/data/route-object#loader)s in parallel - providing
209any data made available via the middleware:
210
211```ts
212const routes = [
213 {
214 id: "parent",
215 path: "/parent",
216 loader({ request }, context) {
217 // ...
218 },
219 handle: {
220 async middleware({ request }, context) {
221 context.parent = "PARENT MIDDLEWARE";
222 },
223 },
224 children: [
225 {
226 id: "child",
227 path: "child",
228 loader({ request }, context) {
229 // ...
230 },
231 handle: {
232 async middleware({ request }, context) {
233 context.child = "CHILD MIDDLEWARE";
234 },
235 },
236 },
237 ],
238 },
239];
240
241let router = createBrowserRouter(routes, {
242 async dataStrategy({ matches, params, request }) {
243 // Run middleware sequentially and let them add data to `context`
244 let context = {};
245 for (const match of matches) {
246 if (match.route.handle?.middleware) {
247 await match.route.handle.middleware(
248 { request, params },
249 context,
250 );
251 }
252 }
253
254 // Run loaders in parallel with the `context` value
255 let matchesToLoad = matches.filter((m) =>
256 m.shouldCallHandler(),
257 );
258 let results = await Promise.all(
259 matchesToLoad.map((match, i) =>
260 match.resolve((handler) => {
261 // Whatever you pass to `handler` will be passed as the 2nd parameter
262 // to your loader/action
263 return handler(context);
264 }),
265 ),
266 );
267 return results.reduce(
268 (acc, result, i) =>
269 Object.assign(acc, {
270 [matchesToLoad[i].route.id]: result,
271 }),
272 {},
273 );
274 },
275});
276```
277
278### Custom Handler
279
280It's also possible you don't even want to define a [`loader`](../start/data/route-object#loader)
281implementation at the route level. Maybe you want to just determine the
282routes and issue a single GraphQL request for all of your data. You can do
283that by setting your `route.loader=true` so it qualifies as "having a
284loader", and then store GQL fragments on `route.handle`:
285
286```ts
287const routes = [
288 {
289 id: "parent",
290 path: "/parent",
291 loader: true,
292 handle: {
293 gql: gql`
294 fragment Parent on Whatever {
295 parentField
296 }
297 `,
298 },
299 children: [
300 {
301 id: "child",
302 path: "child",
303 loader: true,
304 handle: {
305 gql: gql`
306 fragment Child on Whatever {
307 childField
308 }
309 `,
310 },
311 },
312 ],
313 },
314];
315
316let router = createBrowserRouter(routes, {
317 async dataStrategy({ matches, params, request }) {
318 const matchesToLoad = matches.filter((m) =>
319 m.shouldCallHandler(),
320 );
321 // Compose route fragments into a single GQL payload
322 let gql = getFragmentsFromRouteHandles(matchesToLoad);
323 let data = await fetchGql(gql);
324 // Parse results back out into individual route level `DataStrategyResult`'s
325 // keyed by `routeId`
326 let results = parseResultsFromGql(matchesToLoad, data);
327 return results;
328 },
329});
330```
331
332Note that we never actually call `match.resolve()` in this scenario since we don't want to call the handlers defined on the routes. We instead make a single GQL call and split the resulting data back out to the proper routes in `results`.
333
334[loader]: ../start/data/route-object#loader
335[action]: ../start/data/route-object#action
336[data-strategy]: ../api/data-routers/createBrowserRouter#optsdatastrategy