diff --git a/.changeset/nasty-queens-leave.md b/.changeset/nasty-queens-leave.md new file mode 100644 index 0000000000..73c5f677d4 --- /dev/null +++ b/.changeset/nasty-queens-leave.md @@ -0,0 +1,7 @@ +--- +"react-router-dom": minor +"react-router": minor +"@remix-run/router": minor +--- + +Stabilize `unstable_dataStrategy` diff --git a/docs/routers/create-browser-router.md b/docs/routers/create-browser-router.md index 990e2bafd5..31700b4bc9 100644 --- a/docs/routers/create-browser-router.md +++ b/docs/routers/create-browser-router.md @@ -51,7 +51,7 @@ function createBrowserRouter( basename?: string; future?: FutureConfig; hydrationData?: HydrationState; - unstable_dataStrategy?: unstable_DataStrategyFunction; + dataStrategy?: DataStrategyFunction; patchRoutesOnNavigation?: PatchRoutesOnNavigationFunction; window?: Window; } @@ -184,7 +184,7 @@ const router = createBrowserRouter( ); ``` -## `opts.unstable_dataStrategy` +## `opts.dataStrategy` This is a low-level API intended for advanced use-cases. This overrides React Router's internal handling of `loader`/`action` execution, and if done incorrectly will break your app code. Please use with caution and perform the appropriate testing. @@ -192,7 +192,7 @@ const router = createBrowserRouter( By default, React Router is opinionated about how your data is loaded/submitted - and most notably, executes all of your loaders 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. -The `unstable_dataStrategy` option gives you full control over how your loaders and actions 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 applications data needs. +The `dataStrategy` option gives you full control over how your loaders and actions 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 applications data needs. ### Type Declaration @@ -232,7 +232,7 @@ interface DataStrategyResult { ### Overview -`unstable_dataStrategy` receives the same arguments as a `loader`/`action` (`request`, `params`) but it also receives 2 new parameters: `matches` and `fetcherKey`: +`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 @@ -247,9 +247,9 @@ interface DataStrategyResult { - 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 +- **`fetcherKey`** - The key of the fetcher we are calling `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`. +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 `data` utility to return your decoded data along with a `ResponseInit`. ### Example Use Cases @@ -259,7 +259,7 @@ In the simplest case, let's look at hooking into this API to add some logging fo ```ts let router = createBrowserRouter(routes, { - async unstable_dataStrategy({ request, matches }) { + async dataStrategy({ request, matches }) { // Grab only the matches we need to run handlers for const matchesToLoad = matches.filter( (m) => m.shouldLoad @@ -290,7 +290,7 @@ If you want to avoid the `reduce`, you can manually build up the `results` objec ```ts let router = createBrowserRouter(routes, { - async unstable_dataStrategy({ request, matches }) { + async dataStrategy({ request, matches }) { const matchesToLoad = matches.filter( (m) => m.shouldLoad ); @@ -353,11 +353,7 @@ const routes = [ ]; let router = createBrowserRouter(routes, { - async unstable_dataStrategy({ - request, - params, - matches, - }) { + async dataStrategy({ request, params, matches }) { // Run middleware sequentially and let them add data to `context` let context = {}; for (const match of matches) { @@ -426,7 +422,7 @@ const routes = [ ]; let router = createBrowserRouter(routes, { - unstable_dataStrategy({ request, params, matches }) { + dataStrategy({ request, params, matches }) { // Compose route fragments into a single GQL payload let gql = getFragmentsFromRouteHandles(matches); let data = await fetchGql(gql); @@ -481,10 +477,7 @@ const router = createBrowserRouter( }, ], { - async patchRoutesOnNavigation({ - path, - patch, - }) { + async patchRoutesOnNavigation({ path, patch }) { if (path === "/a") { // Load/patch the `a` route as a child of the route with id `root` let route = await getARoute(); @@ -512,10 +505,7 @@ const router = createBrowserRouter( }, ], { - async patchRoutesOnNavigation({ - path, - patch, - }) { + async patchRoutesOnNavigation({ path, patch }) { if (path === "/root-sibling") { // Load/patch the `/root-sibling` route as a sibling of the root route let route = await getRootSiblingRoute(); @@ -540,10 +530,7 @@ let router = createBrowserRouter( }, ], { - async patchRoutesOnNavigation({ - path, - patch, - }) { + async patchRoutesOnNavigation({ path, patch }) { if (path.startsWith("/dashboard")) { let children = await import("./dashboard"); patch(null, children); @@ -598,10 +585,7 @@ let router = createBrowserRouter( }, ], { - async patchRoutesOnNavigation({ - matches, - patch, - }) { + async patchRoutesOnNavigation({ matches, patch }) { let leafRoute = matches[matches.length - 1]?.route; if (leafRoute?.handle?.lazyChildren) { let children = diff --git a/packages/react-router-dom-v5-compat/index.ts b/packages/react-router-dom-v5-compat/index.ts index 5c1dc7e565..62d4cf1814 100644 --- a/packages/react-router-dom-v5-compat/index.ts +++ b/packages/react-router-dom-v5-compat/index.ts @@ -51,10 +51,10 @@ export type { ActionFunctionArgs, AwaitProps, BrowserRouterProps, - unstable_DataStrategyFunction, - unstable_DataStrategyFunctionArgs, - unstable_DataStrategyMatch, - unstable_DataStrategyResult, + DataStrategyFunction, + DataStrategyFunctionArgs, + DataStrategyMatch, + DataStrategyResult, DataRouteMatch, DataRouteObject, ErrorResponse, diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index 1532fe6206..6b30da81d2 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -16,7 +16,7 @@ import type { RouterProps, RouterProviderProps, To, - unstable_DataStrategyFunction, + DataStrategyFunction, PatchRoutesOnNavigationFunction, } from "react-router"; import { @@ -106,10 +106,10 @@ export type { BlockerFunction, DataRouteMatch, DataRouteObject, - unstable_DataStrategyFunction, - unstable_DataStrategyFunctionArgs, - unstable_DataStrategyMatch, - unstable_DataStrategyResult, + DataStrategyFunction, + DataStrategyFunctionArgs, + DataStrategyMatch, + DataStrategyResult, ErrorResponse, Fetcher, FutureConfig, @@ -258,7 +258,7 @@ interface DOMRouterOpts { basename?: string; future?: Partial>; hydrationData?: HydrationState; - unstable_dataStrategy?: unstable_DataStrategyFunction; + dataStrategy?: DataStrategyFunction; patchRoutesOnNavigation?: PatchRoutesOnNavigationFunction; window?: Window; } @@ -277,7 +277,7 @@ export function createBrowserRouter( hydrationData: opts?.hydrationData || parseHydrationData(), routes, mapRouteProperties, - unstable_dataStrategy: opts?.unstable_dataStrategy, + dataStrategy: opts?.dataStrategy, patchRoutesOnNavigation: opts?.patchRoutesOnNavigation, window: opts?.window, }).initialize(); @@ -297,7 +297,7 @@ export function createHashRouter( hydrationData: opts?.hydrationData || parseHydrationData(), routes, mapRouteProperties, - unstable_dataStrategy: opts?.unstable_dataStrategy, + dataStrategy: opts?.dataStrategy, patchRoutesOnNavigation: opts?.patchRoutesOnNavigation, window: opts?.window, }).initialize(); diff --git a/packages/react-router-native/index.tsx b/packages/react-router-native/index.tsx index 6a9445c085..be3d8bc2c1 100644 --- a/packages/react-router-native/index.tsx +++ b/packages/react-router-native/index.tsx @@ -27,10 +27,10 @@ export type { BlockerFunction, DataRouteMatch, DataRouteObject, - unstable_DataStrategyFunction, - unstable_DataStrategyFunctionArgs, - unstable_DataStrategyMatch, - unstable_DataStrategyResult, + DataStrategyFunction, + DataStrategyFunctionArgs, + DataStrategyMatch, + DataStrategyResult, ErrorResponse, Fetcher, FutureConfig, diff --git a/packages/react-router/__tests__/data-memory-router-test.tsx b/packages/react-router/__tests__/data-memory-router-test.tsx index 9f1a9e8b71..aaca20ccd9 100644 --- a/packages/react-router/__tests__/data-memory-router-test.tsx +++ b/packages/react-router/__tests__/data-memory-router-test.tsx @@ -3210,7 +3210,7 @@ describe("createMemoryRouter", () => { /> ), - { initialEntries: ["/foo"], unstable_dataStrategy: urlDataStrategy } + { initialEntries: ["/foo"], dataStrategy: urlDataStrategy } ); let { container } = render(); diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts index aef4408dfd..79cca46235 100644 --- a/packages/react-router/index.ts +++ b/packages/react-router/index.ts @@ -4,10 +4,10 @@ import type { ActionFunctionArgs, Blocker, BlockerFunction, - unstable_DataStrategyFunction, - unstable_DataStrategyFunctionArgs, - unstable_DataStrategyMatch, - unstable_DataStrategyResult, + DataStrategyFunction, + DataStrategyFunctionArgs, + DataStrategyMatch, + DataStrategyResult, ErrorResponse, Fetcher, HydrationState, @@ -136,10 +136,10 @@ export type { AwaitProps, DataRouteMatch, DataRouteObject, - unstable_DataStrategyFunction, - unstable_DataStrategyFunctionArgs, - unstable_DataStrategyMatch, - unstable_DataStrategyResult, + DataStrategyFunction, + DataStrategyFunctionArgs, + DataStrategyMatch, + DataStrategyResult, ErrorResponse, Fetcher, FutureConfig, @@ -302,7 +302,7 @@ export function createMemoryRouter( hydrationData?: HydrationState; initialEntries?: InitialEntry[]; initialIndex?: number; - unstable_dataStrategy?: unstable_DataStrategyFunction; + dataStrategy?: DataStrategyFunction; patchRoutesOnNavigation?: PatchRoutesOnNavigationFunction; } ): RemixRouter { @@ -319,7 +319,7 @@ export function createMemoryRouter( hydrationData: opts?.hydrationData, routes, mapRouteProperties, - unstable_dataStrategy: opts?.unstable_dataStrategy, + dataStrategy: opts?.dataStrategy, patchRoutesOnNavigation: opts?.patchRoutesOnNavigation, }).initialize(); } diff --git a/packages/router/__tests__/ssr-test.ts b/packages/router/__tests__/ssr-test.ts index df7c1882b5..b8f21f0ea3 100644 --- a/packages/router/__tests__/ssr-test.ts +++ b/packages/router/__tests__/ssr-test.ts @@ -1602,7 +1602,7 @@ describe("ssr", () => { let { query } = createStaticHandler(SSR_ROUTES); let context = await query(createRequest("/custom"), { - unstable_dataStrategy: urlDataStrategy, + dataStrategy: urlDataStrategy, }); expect(context).toMatchObject({ actionData: null, @@ -2635,7 +2635,7 @@ describe("ssr", () => { let data; data = await queryRoute(createRequest("/custom"), { - unstable_dataStrategy: urlDataStrategy, + dataStrategy: urlDataStrategy, }); expect(data).toBeInstanceOf(URLSearchParams); expect((data as URLSearchParams).get("foo")).toBe("bar"); diff --git a/packages/router/__tests__/utils/data-router-setup.ts b/packages/router/__tests__/utils/data-router-setup.ts index cc77d8f290..b97cda98e2 100644 --- a/packages/router/__tests__/utils/data-router-setup.ts +++ b/packages/router/__tests__/utils/data-router-setup.ts @@ -322,7 +322,7 @@ export function setup({ hydrationData, future, window: testWindow, - unstable_dataStrategy: dataStrategy, + dataStrategy: dataStrategy, }).initialize(); function getRouteHelpers( diff --git a/packages/router/index.ts b/packages/router/index.ts index 51608bc6e5..dbdda02610 100644 --- a/packages/router/index.ts +++ b/packages/router/index.ts @@ -9,10 +9,10 @@ export type { AgnosticNonIndexRouteObject, AgnosticRouteMatch, AgnosticRouteObject, - DataStrategyFunction as unstable_DataStrategyFunction, - DataStrategyFunctionArgs as unstable_DataStrategyFunctionArgs, - DataStrategyMatch as unstable_DataStrategyMatch, - DataStrategyResult as unstable_DataStrategyResult, + DataStrategyFunction, + DataStrategyFunctionArgs, + DataStrategyMatch, + DataStrategyResult, ErrorResponse, FormEncType, FormMethod, @@ -38,7 +38,7 @@ export type { export { AbortedDeferredError, - data as unstable_data, + data, defer, generatePath, getToPathname, diff --git a/packages/router/router.ts b/packages/router/router.ts index 7551725ca4..8502da54eb 100644 --- a/packages/router/router.ts +++ b/packages/router/router.ts @@ -391,8 +391,8 @@ export interface RouterInit { future?: Partial; hydrationData?: HydrationState; window?: Window; + dataStrategy?: DataStrategyFunction; patchRoutesOnNavigation?: AgnosticPatchRoutesOnNavigationFunction; - unstable_dataStrategy?: DataStrategyFunction; } /** @@ -422,7 +422,7 @@ export interface StaticHandler { opts?: { requestContext?: unknown; skipLoaderErrorBubbling?: boolean; - unstable_dataStrategy?: DataStrategyFunction; + dataStrategy?: DataStrategyFunction; } ): Promise; queryRoute( @@ -430,7 +430,7 @@ export interface StaticHandler { opts?: { routeId?: string; requestContext?: unknown; - unstable_dataStrategy?: DataStrategyFunction; + dataStrategy?: DataStrategyFunction; } ): Promise; } @@ -797,7 +797,7 @@ export function createRouter(init: RouterInit): Router { ); let inFlightDataRoutes: AgnosticDataRouteObject[] | undefined; let basename = init.basename || "/"; - let dataStrategyImpl = init.unstable_dataStrategy || defaultDataStrategy; + let dataStrategyImpl = init.dataStrategy || defaultDataStrategy; let patchRoutesOnNavigationImpl = init.patchRoutesOnNavigation; // Config driven behavior flags @@ -3538,11 +3538,11 @@ export function createStaticHandler( { requestContext, skipLoaderErrorBubbling, - unstable_dataStrategy, + dataStrategy, }: { requestContext?: unknown; skipLoaderErrorBubbling?: boolean; - unstable_dataStrategy?: DataStrategyFunction; + dataStrategy?: DataStrategyFunction; } = {} ): Promise { let url = new URL(request.url); @@ -3594,7 +3594,7 @@ export function createStaticHandler( location, matches, requestContext, - unstable_dataStrategy || null, + dataStrategy || null, skipLoaderErrorBubbling === true, null ); @@ -3639,11 +3639,11 @@ export function createStaticHandler( { routeId, requestContext, - unstable_dataStrategy, + dataStrategy, }: { requestContext?: unknown; routeId?: string; - unstable_dataStrategy?: DataStrategyFunction; + dataStrategy?: DataStrategyFunction; } = {} ): Promise { let url = new URL(request.url); @@ -3677,7 +3677,7 @@ export function createStaticHandler( location, matches, requestContext, - unstable_dataStrategy || null, + dataStrategy || null, false, match ); @@ -3716,7 +3716,7 @@ export function createStaticHandler( location: Location, matches: AgnosticDataRouteMatch[], requestContext: unknown, - unstable_dataStrategy: DataStrategyFunction | null, + dataStrategy: DataStrategyFunction | null, skipLoaderErrorBubbling: boolean, routeMatch: AgnosticDataRouteMatch | null ): Promise | Response> { @@ -3732,7 +3732,7 @@ export function createStaticHandler( matches, routeMatch || getTargetMatch(matches, location), requestContext, - unstable_dataStrategy, + dataStrategy, skipLoaderErrorBubbling, routeMatch != null ); @@ -3743,7 +3743,7 @@ export function createStaticHandler( request, matches, requestContext, - unstable_dataStrategy, + dataStrategy, skipLoaderErrorBubbling, routeMatch ); @@ -3778,7 +3778,7 @@ export function createStaticHandler( matches: AgnosticDataRouteMatch[], actionMatch: AgnosticDataRouteMatch, requestContext: unknown, - unstable_dataStrategy: DataStrategyFunction | null, + dataStrategy: DataStrategyFunction | null, skipLoaderErrorBubbling: boolean, isRouteRequest: boolean ): Promise | Response> { @@ -3805,7 +3805,7 @@ export function createStaticHandler( matches, isRouteRequest, requestContext, - unstable_dataStrategy + dataStrategy ); result = results[actionMatch.route.id]; @@ -3877,7 +3877,7 @@ export function createStaticHandler( loaderRequest, matches, requestContext, - unstable_dataStrategy, + dataStrategy, skipLoaderErrorBubbling, null, [boundaryMatch.route.id, result] @@ -3902,7 +3902,7 @@ export function createStaticHandler( loaderRequest, matches, requestContext, - unstable_dataStrategy, + dataStrategy, skipLoaderErrorBubbling, null ); @@ -3924,7 +3924,7 @@ export function createStaticHandler( request: Request, matches: AgnosticDataRouteMatch[], requestContext: unknown, - unstable_dataStrategy: DataStrategyFunction | null, + dataStrategy: DataStrategyFunction | null, skipLoaderErrorBubbling: boolean, routeMatch: AgnosticDataRouteMatch | null, pendingActionResult?: PendingActionResult @@ -3987,7 +3987,7 @@ export function createStaticHandler( matches, isRouteRequest, requestContext, - unstable_dataStrategy + dataStrategy ); if (request.signal.aborted) { @@ -4033,10 +4033,10 @@ export function createStaticHandler( matches: AgnosticDataRouteMatch[], isRouteRequest: boolean, requestContext: unknown, - unstable_dataStrategy: DataStrategyFunction | null + dataStrategy: DataStrategyFunction | null ): Promise> { let results = await callDataStrategyImpl( - unstable_dataStrategy || defaultDataStrategy, + dataStrategy || defaultDataStrategy, type, null, request, @@ -5038,7 +5038,7 @@ async function convertDataStrategyResultToDataResult( }; } - // Convert thrown unstable_data() to ErrorResponse instances + // Convert thrown data() to ErrorResponse instances result = new ErrorResponseImpl( result.init?.status || 500, undefined,