UNPKG

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