Skip to content

Commit

Permalink
unstable_dataStrategy refactor for better single fetch support (#11943)
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 committed Sep 4, 2024
1 parent 766f07d commit 05612a3
Show file tree
Hide file tree
Showing 12 changed files with 581 additions and 351 deletions.
14 changes: 14 additions & 0 deletions .changeset/four-books-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@remix-run/router": patch
---

Update the `unstable_dataStrategy` API to allow for more advanced implementations

- Rename `unstable_HandlerResult` to `unstable_DataStrategyResult`
- The return signature has changed from a parallel array of `unstable_DataStrategyResult[]` (parallel to `matches`) to a key/value object of `routeId => unstable_DataStrategyResult`
- This allows you to more easily decide to opt-into or out-of revalidating data that may not have been revalidated by default (via `match.shouldLoad`)
- ⚠️ This is a breaking change if you've currently adopted `unstable_dataStrategy`
- Added a new `fetcherKey` parameter to `unstable_dataStrategy` to allow differentiation from navigational and fetcher calls
- You should now return/throw a result from your `handlerOverride` instead of returning a `DataStrategyResult`
- If you are aggregating the results of `match.resolve()` into a final results object you should not need to think about the `DataStrategyResult` type
- If you are manually filling your results object from within your `handlerOverride`, then you will need to assign a `DataStrategyResult` as the value so React Router knows if it's a successful execution or an error.
124 changes: 89 additions & 35 deletions docs/routers/create-browser-router.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ The `unstable_dataStrategy` option gives you full control over how your loaders
```ts
interface DataStrategyFunction {
(args: DataStrategyFunctionArgs): Promise<
HandlerResult[]
Record<string, DataStrategyResult>
>;
}

Expand All @@ -208,6 +208,7 @@ interface DataStrategyFunctionArgs<Context = any> {
params: Params;
context?: Context;
matches: DataStrategyMatch[];
fetcherKey: string | null;
}

interface DataStrategyMatch
Expand All @@ -219,34 +220,36 @@ interface DataStrategyMatch
resolve: (
handlerOverride?: (
handler: (ctx?: unknown) => DataFunctionReturnValue
) => Promise<HandlerResult>
) => Promise<HandlerResult>;
) => Promise<DataStrategyResult>
) => Promise<DataStrategyResult>;
}

interface HandlerResult {
interface DataStrategyResult {
type: "data" | "error";
result: any; // data, Error, Response, DeferredData
status?: number;
result: unknown; // data, Error, Response, DeferredData, DataWithResponseInit
}
```

### Overview

`unstable_dataStrategy` receives the same arguments as a `loader`/`action` (`request`, `params`) but it also receives a `matches` array which is an array of the matched routes where each match is extended with 2 new fields for use in the data strategy function:

- **`match.resolve`** - An async function that will resolve any `route.lazy` implementations and execute the route's handler (if necessary), returning a `HandlerResult`
- You should call `match.resolve` for _all_ matches every time to ensure that all lazy routes are properly resolved
- This does not mean you're calling the loader/action (the "handler") - `resolve` will only call the `handler` internally if needed and if you don't pass your own `handlerOverride` function parameter
- See the examples below for how to implement custom handler execution via `match.resolve`
- **`match.shouldLoad`** - A boolean value indicating whether this route handler needs to be called in this pass
- The `matches` array always includes _all_ matched routes even when only _some_ route handlers need to be called so that things like middleware can be implemented
- `shouldLoad` is usually only interesting if you are skipping the route handler entirely and implementing custom handler logic - since it lets you determine if that custom logic should run for this route or not
- For example:
- If you are on `/parent/child/a` and you navigate to `/parent/child/b` - you'll get an array of three matches (`[parent, child, b]`), but only `b` will have `shouldLoad=true` because the data for `parent` and `child` is already loaded
- If you are on `/parent/child/a` and you submit to `a`'s `action`, then only `a` will have `shouldLoad=true` for the action execution of `dataStrategy`
- After the `action`, `dataStrategy` will be called again for the `loader` revalidation, and all matches will have `shouldLoad=true` (assuming no custom `shouldRevalidate` implementations)

The `dataStrategy` function should return a parallel array of `HandlerResult` instances, which indicates if the handler was successful or not. If the returned `handlerResult.result` is a `Response`, React Router will unwrap it for you (via `res.json` or `res.text`). If you need to do custom decoding of a `Response` but preserve the status code, you can return the decoded value in `handlerResult.result` and send the status along via `handlerResult.status` (for example, when using the `future.v7_skipActionRevalidation` flag). `match.resolve()` will return a `HandlerResult` if you are not passing it a handler override function. If you are, then you need to wrap the `handler` result in a `HandlerResult` (see examples below).
`unstable_dataStrategy` receives the same arguments as a `loader`/`action` (`request`, `params`) but it also receives 2 new parameters: `matches` and `fetcherKey`:

- **`matches`** - An array of the matched routes where each match is extended with 2 new fields for use in the data strategy function:
- **`match.shouldLoad`** - A boolean value indicating whether this route handler should be called in this pass
- The `matches` array always includes _all_ matched routes even when only _some_ route handlers need to be called so that things like middleware can be implemented
- `shouldLoad` is usually only interesting if you are skipping the route handler entirely and implementing custom handler logic - since it lets you determine if that custom logic should run for this route or not
- For example:
- If you are on `/parent/child/a` and you navigate to `/parent/child/b` - you'll get an array of three matches (`[parent, child, b]`), but only `b` will have `shouldLoad=true` because the data for `parent` and `child` is already loaded
- If you are on `/parent/child/a` and you submit to `a`'s `action`, then only `a` will have `shouldLoad=true` for the action execution of `dataStrategy`
- After the `action`, `dataStrategy` will be called again for the `loader` revalidation, and all matches will have `shouldLoad=true` (assuming no custom `shouldRevalidate` implementations)
- **`match.resolve`** - An async function that will resolve any `route.lazy` implementations and execute the route's handler (if necessary), returning a `DataStrategyResult`
- Calling `match.resolve` does not mean you're calling the `loader`/`action` (the "handler") - `resolve` will only call the `handler` internally if needed _and_ if you don't pass your own `handlerOverride` function parameter
- It is safe to call `match.resolve` for all matches, even if they have `shouldLoad=false`, and it will no-op if no loading is required
- You should generally always call `match.resolve()` for `shouldLoad:true` routes to ensure that any `route.lazy` implementations are processed
- See the examples below for how to implement custom handler execution via `match.resolve`
- **`fetcherKey`** - The key of the fetcher we are calling `unstable_dataStrategy` for, otherwise `null` for navigational executions

The `dataStrategy` function should return a key/value object of `routeId -> DataStrategyResult` and should include entries for any routes where a handler was executed. A `DataStrategyResult` indicates if the handler was successful or not based on the `DataStrategyResult["type"]` field. If the returned `DataStrategyResult["result"]` is a `Response`, React Router will unwrap it for you (via `res.json` or `res.text`). If you need to do custom decoding of a `Response` but want to preserve the status code, you can use the `unstable_data` utility to return your decoded data along with a `ResponseInit`.

### Example Use Cases

Expand All @@ -256,18 +259,61 @@ In the simplest case, let's look at hooking into this API to add some logging fo

```ts
let router = createBrowserRouter(routes, {
unstable_dataStrategy({ request, matches }) {
return Promise.all(
matches.map(async (match) => {
console.log(`Processing route ${match.route.id}`);
async unstable_dataStrategy({ request, matches }) {
// Grab only the matches we need to run handlers for
const matchesToLoad = matches.filter(
(m) => m.shouldLoad
);
// Run the handlers in parallel, logging before and after
const results = await Promise.all(
matchesToLoad.map(async (match) => {
console.log(`Processing ${match.route.id}`);
// Don't override anything - just resolve route.lazy + call loader
let result = await match.resolve();
console.log(
`Done processing route ${match.route.id}`
);
const result = await match.resolve();
return result;
})
);

// Aggregate the results into a bn object of `routeId -> DataStrategyResult`
return results.reduce(
(acc, result, i) =>
Object.assign(acc, {
[matchesToLoad[i].route.id]: result,
}),
{}
);
},
});
```

If you want to avoid the `reduce`, you can manually build up the `results` object, but you'll need to construct the `DataStrategyResult` manually - indicating if the handler was successful or not:

```ts
let router = createBrowserRouter(routes, {
async unstable_dataStrategy({ request, matches }) {
const matchesToLoad = matches.filter(
(m) => m.shouldLoad
);
const results = {};
await Promise.all(
matchesToLoad.map(async (match) => {
console.log(`Processing ${match.route.id}`);
try {
const result = await match.resolve();
results[match.route.id] = {
type: "data",
result,
};
} catch (e) {
results[match.route.id] = {
type: "error",
result: e,
};
}
})
);

return results;
},
});
```
Expand Down Expand Up @@ -324,16 +370,23 @@ let router = createBrowserRouter(routes, {
}

// Run loaders in parallel with the `context` value
return Promise.all(
matches.map((match, i) =>
match.resolve(async (handler) => {
let matchesToLoad = matches.filter((m) => m.shouldLoad);
let results = await Promise.all(
matchesToLoad.map((match, i) =>
match.resolve((handler) => {
// Whatever you pass to `handler` will be passed as the 2nd parameter
// to your loader/action
let result = await handler(context);
return { type: "data", result };
return handler(context);
})
)
);
return results.reduce(
(acc, result, i) =>
Object.assign(acc, {
[matchesToLoad[i].route.id]: result,
}),
{}
);
},
});
```
Expand Down Expand Up @@ -377,7 +430,8 @@ let router = createBrowserRouter(routes, {
// Compose route fragments into a single GQL payload
let gql = getFragmentsFromRouteHandles(matches);
let data = await fetchGql(gql);
// Parse results back out into individual route level HandlerResult's
// Parse results back out into individual route level `DataStrategyResult`'s
// keyed by `routeId`
let results = parseResultsFromGql(data);
return results;
},
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
},
"filesize": {
"packages/router/dist/router.umd.min.js": {
"none": "57.2 kB"
"none": "58.1 kB"
},
"packages/react-router/dist/react-router.production.min.js": {
"none": "15.0 kB"
Expand Down
2 changes: 1 addition & 1 deletion packages/react-router-dom-v5-compat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export type {
unstable_DataStrategyFunction,
unstable_DataStrategyFunctionArgs,
unstable_DataStrategyMatch,
unstable_DataStrategyResult,
DataRouteMatch,
DataRouteObject,
ErrorResponse,
Expand Down Expand Up @@ -112,7 +113,6 @@ export type {
UIMatch,
Blocker,
BlockerFunction,
unstable_HandlerResult,
} from "./react-router-dom";
export {
AbortedDeferredError,
Expand Down
12 changes: 5 additions & 7 deletions packages/react-router-dom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
RouterProps,
RouterProviderProps,
To,
unstable_DataStrategyFunction,
unstable_PatchRoutesOnNavigationFunction,
} from "react-router";
import {
Expand All @@ -38,9 +39,6 @@ import {
} from "react-router";
import type {
BrowserHistory,
unstable_DataStrategyFunction,
unstable_DataStrategyFunctionArgs,
unstable_DataStrategyMatch,
Fetcher,
FormEncType,
FormMethod,
Expand Down Expand Up @@ -89,9 +87,6 @@ import {
////////////////////////////////////////////////////////////////////////////////

export type {
unstable_DataStrategyFunction,
unstable_DataStrategyFunctionArgs,
unstable_DataStrategyMatch,
FormEncType,
FormMethod,
GetScrollRestorationKeyFunction,
Expand All @@ -111,6 +106,10 @@ export type {
BlockerFunction,
DataRouteMatch,
DataRouteObject,
unstable_DataStrategyFunction,
unstable_DataStrategyFunctionArgs,
unstable_DataStrategyMatch,
unstable_DataStrategyResult,
ErrorResponse,
Fetcher,
FutureConfig,
Expand Down Expand Up @@ -152,7 +151,6 @@ export type {
ShouldRevalidateFunctionArgs,
To,
UIMatch,
unstable_HandlerResult,
unstable_PatchRoutesOnNavigationFunction,
} from "react-router";
export {
Expand Down
2 changes: 1 addition & 1 deletion packages/react-router-native/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type {
unstable_DataStrategyFunction,
unstable_DataStrategyFunctionArgs,
unstable_DataStrategyMatch,
unstable_DataStrategyResult,
ErrorResponse,
Fetcher,
FutureConfig,
Expand Down Expand Up @@ -71,7 +72,6 @@ export type {
ShouldRevalidateFunctionArgs,
To,
UIMatch,
unstable_HandlerResult,
} from "react-router";
export {
AbortedDeferredError,
Expand Down
4 changes: 2 additions & 2 deletions packages/react-router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
unstable_DataStrategyFunction,
unstable_DataStrategyFunctionArgs,
unstable_DataStrategyMatch,
unstable_DataStrategyResult,
ErrorResponse,
Fetcher,
HydrationState,
Expand All @@ -31,7 +32,6 @@ import type {
ShouldRevalidateFunctionArgs,
To,
UIMatch,
unstable_HandlerResult,
unstable_AgnosticPatchRoutesOnNavigationFunction,
} from "@remix-run/router";
import {
Expand Down Expand Up @@ -139,6 +139,7 @@ export type {
unstable_DataStrategyFunction,
unstable_DataStrategyFunctionArgs,
unstable_DataStrategyMatch,
unstable_DataStrategyResult,
ErrorResponse,
Fetcher,
FutureConfig,
Expand Down Expand Up @@ -182,7 +183,6 @@ export type {
UIMatch,
Blocker,
BlockerFunction,
unstable_HandlerResult,
};
export {
AbortedDeferredError,
Expand Down
Loading

0 comments on commit 05612a3

Please sign in to comment.