INDIE UNPKG

13.3 kBMarkdownView Raw
1---
2title: Data Strategy
3---
4
5# Data Strategy
6
7[MODES: data]
8
9<br />
10<br />
11
12<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>
13
14## Overview
15
16By 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.
17
18The [`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.
19
20## Usage
21
22A 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:
23
24- `matches`: An array of `DataStrategyMatch` instances for the routes matched by the current `request`
25- `runClientMiddleware`: A helper function to run the middleware for the matched routes
26- `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.
27- `unstable_initiator`: The operation that initiated this execution (see [Request Metadata](#request-metadata-unstable))
28- `unstable_navigationType`: The history action for a navigation-initiated execution, or `null` for other executions
29
30A `DataStrategyMatch` is a normal route match plus a few additional fields:
31
32- `shouldCallHandler`: A function that tells you whether this routes handler should be called for this request
33- `shouldRevalidateArgs`: The arguments that to be passed to the routes `shouldRevalidate` for this request
34- ~~`shouldLoad`~~: A boolean field for whether this routes handler should be run for this request
35 - Deprecated in favor of the more powerful `shouldCallHandler` API
36- `resolve`: A function to handle call through to the route handler, and also allow you custom execution of the handler
37
38Here's a basic example that adds logging around the handler executions:
39
40```tsx
41let router = createBrowserRouter(routes, {
42 async dataStrategy({
43 matches,
44 request,
45 runClientMiddleware,
46 }) {
47 // Determine which matches are expected to be executed for this request.
48 // - For loading navigations, this will return true for new routes + existing
49 // routes requiring revalidation
50 // - For submission navigations, this will only return true for the action route
51 // - For fetcher calls, this will only return true for the fetcher route
52 const matchesToLoad = matches.filter((m) =>
53 m.shouldCallHandler(),
54 );
55
56 // For each match that we want to execute, call match.resolve() to execute
57 // the handler and store the result
58 const results: Record<string, DataStrategyResult> = {};
59 await runClientMiddleware(() =>
60 Promise.all(
61 matchesToLoad.map(async (match) => {
62 console.log(`Processing ${match.route.id}`);
63 // The resolve function calls through to the route handler
64 results[match.route.id] = await match.resolve();
65 }),
66 ),
67 );
68 return results;
69 },
70});
71```
72
73The `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:
74
75```ts
76interface DataStrategyResult {
77 type: "data" | "error";
78 result: unknown; // data, Error, Response, data()
79}
80```
81
82### Request Metadata (unstable)
83
84<docs-warning>`unstable_initiator` and `unstable_navigationType` are experimental and may change in minor or patch releases.</docs-warning>
85
86`unstable_initiator` identifies why the data strategy is running:
87
88- `"initialization"`: Initial router data loading
89- `"navigation"`: A navigation, including action submissions and subsequent loader revalidation
90- `"fetcher"`: A fetcher operation, including loader revalidation after a fetcher action
91- `"revalidation"`: Explicit revalidation, such as `useRevalidator().revalidate()`
92- `"static"`: Execution by `createStaticHandler` on the server
93
94Requests 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.
95
96`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.
97
98You 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.
99
100Framework Mode exposes equivalent request metadata through [`unstable_fetch`](./custom-fetch#request-context), which customizes server requests without replacing loader/action execution.
101
102### Calling Route Middleware
103
104If you are using `middleware` on your routes, you need to leverage the `callClientMiddleware` helper function to execute `middleware` around your handlers:
105
106```tsx
107let router = createBrowserRouter(routes, {
108 async dataStrategy({
109 matches,
110 request,
111 runClientMiddleware,
112 }) {
113 const matchesToLoad = matches.filter((m) =>
114 m.shouldCallHandler(),
115 );
116 const results: Record<string, DataStrategyResult> = {};
117
118 // Run middleware and execute handlers at the end of the middleware chain
119 await runClientMiddleware(() =>
120 Promise.all(
121 matchesToLoad.map(async (match) => {
122 results[match.route.id] = await match.resolve();
123 }),
124 ),
125 );
126 return results;
127 },
128});
129```
130
131`runClientMiddleware` takes the same arguments as `dataStrategy` so it can also be easily composed with a standalone `dataStrategy` implementation:
132
133```tsx
134const loggingDataStrategy: DataStrategyFunction = () => {
135 /* ... */
136};
137
138let router = createBrowserRouter(routes, {
139 async dataStrategy({ runClientMiddleware }) {
140 let results = await runClientMiddleware(
141 loggingDataStrategy,
142 );
143 return results;
144 },
145});
146```
147
148### Advanced handler execution
149
150If you want more fine-grained control over the execution of the handler, you can pass a callback to `match.resolve()`:
151
152```tsx
153// Assume a loader shape such as
154function loader({ request }, customContext) {...}
155
156// In your dataStrategy, you can pass this context from inside a resolve callback
157await Promise.all(
158 matchesToLoad.map((match, i) =>
159 match.resolve((handler) => {
160 let customContext = getCustomContext();
161 // Call the handler and p[ass a custom parameter as the handler's second argument
162 return handler(customContext);
163 }),
164 ),
165);
166```
167
168### Custom Revalidation Behavior
169
170If 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`:
171
172```tsx
173const matchesToLoad = matches.filter((match) => {
174 let defaultShouldRevalidate = customShouldRevalidate(
175 match.shouldRevalidateArgs,
176 );
177 return m.shouldCallHandler(defaultShouldRevalidate);
178});
179```
180
181## Migrating away from `shouldLoad`
182
183Now 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.
184
185The 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.
186
187With `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.
188
189Here's an example change from the prior API to the new API. Note that we pre-filter the `matchesToLoad` before calling `resolve()`:
190
191```tsx diff
192let results = {};
193+let matchesToLoad = matches.filter(m => m.shouldCallHandler());
194await Promise.all(() =>
195- matches.map((m) => {
196+ matchesToLoad.map((m) => {
197 results[m.route.id] = await m.resolve();
198 }),
199);
200return results;
201```
202
203## Advanced Use Cases
204
205### Custom Middleware
206
207<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>
208
209Let's define a middleware on each route via [`handle`](../start/data/route-object#handle)
210and call middleware sequentially first, then call all
211[`loader`](../start/data/route-object#loader)s in parallel - providing
212any data made available via the middleware:
213
214```ts
215const routes = [
216 {
217 id: "parent",
218 path: "/parent",
219 loader({ request }, context) {
220 // ...
221 },
222 handle: {
223 async middleware({ request }, context) {
224 context.parent = "PARENT MIDDLEWARE";
225 },
226 },
227 children: [
228 {
229 id: "child",
230 path: "child",
231 loader({ request }, context) {
232 // ...
233 },
234 handle: {
235 async middleware({ request }, context) {
236 context.child = "CHILD MIDDLEWARE";
237 },
238 },
239 },
240 ],
241 },
242];
243
244let router = createBrowserRouter(routes, {
245 async dataStrategy({ matches, params, request }) {
246 // Run middleware sequentially and let them add data to `context`
247 let context = {};
248 for (const match of matches) {
249 if (match.route.handle?.middleware) {
250 await match.route.handle.middleware(
251 { request, params },
252 context,
253 );
254 }
255 }
256
257 // Run loaders in parallel with the `context` value
258 let matchesToLoad = matches.filter((m) =>
259 m.shouldCallHandler(),
260 );
261 let results = await Promise.all(
262 matchesToLoad.map((match, i) =>
263 match.resolve((handler) => {
264 // Whatever you pass to `handler` will be passed as the 2nd parameter
265 // to your loader/action
266 return handler(context);
267 }),
268 ),
269 );
270 return results.reduce(
271 (acc, result, i) =>
272 Object.assign(acc, {
273 [matchesToLoad[i].route.id]: result,
274 }),
275 {},
276 );
277 },
278});
279```
280
281### Custom Handler
282
283It's also possible you don't even want to define a [`loader`](../start/data/route-object#loader)
284implementation at the route level. Maybe you want to just determine the
285routes and issue a single GraphQL request for all of your data. You can do
286that by setting your `route.loader=true` so it qualifies as "having a
287loader", and then store GQL fragments on `route.handle`:
288
289```ts
290const routes = [
291 {
292 id: "parent",
293 path: "/parent",
294 loader: true,
295 handle: {
296 gql: gql`
297 fragment Parent on Whatever {
298 parentField
299 }
300 `,
301 },
302 children: [
303 {
304 id: "child",
305 path: "child",
306 loader: true,
307 handle: {
308 gql: gql`
309 fragment Child on Whatever {
310 childField
311 }
312 `,
313 },
314 },
315 ],
316 },
317];
318
319let router = createBrowserRouter(routes, {
320 async dataStrategy({ matches, params, request }) {
321 const matchesToLoad = matches.filter((m) =>
322 m.shouldCallHandler(),
323 );
324 // Compose route fragments into a single GQL payload
325 let gql = getFragmentsFromRouteHandles(matchesToLoad);
326 let data = await fetchGql(gql);
327 // Parse results back out into individual route level `DataStrategyResult`'s
328 // keyed by `routeId`
329 let results = parseResultsFromGql(matchesToLoad, data);
330 return results;
331 },
332});
333```
334
335Note 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`.
336
337[loader]: ../start/data/route-object#loader
338[action]: ../start/data/route-object#action
339[data-strategy]: ../api/data-routers/createBrowserRouter#optsdatastrategy