Push V1 app
This commit is contained in:
+2730
File diff suppressed because it is too large
Load Diff
+23
@@ -0,0 +1,23 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) React Training LLC 2015-2019
|
||||
Copyright (c) Remix Software Inc. 2020-2021
|
||||
Copyright (c) Shopify Inc. 2022-2023
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
`react-router` is the primary package in the React Router project.
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
npm i react-router
|
||||
```
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
import * as React from 'react';
|
||||
import { R as RouterInit } from './instrumentation-Dkmpzd13.js';
|
||||
import { L as Location, C as ClientActionFunction, a as ClientLoaderFunction, b as LinksFunction, M as MetaFunction, S as ShouldRevalidateFunction, P as Params, c as RouterContextProvider, A as ActionFunction, H as HeadersFunction, d as LoaderFunction } from './data-CjO11-hU.js';
|
||||
|
||||
declare function getRequest(): Request;
|
||||
type RSCRouteConfigEntryBase = {
|
||||
action?: ActionFunction;
|
||||
clientAction?: ClientActionFunction;
|
||||
clientLoader?: ClientLoaderFunction;
|
||||
ErrorBoundary?: React.ComponentType<any>;
|
||||
handle?: any;
|
||||
headers?: HeadersFunction;
|
||||
HydrateFallback?: React.ComponentType<any>;
|
||||
Layout?: React.ComponentType<any>;
|
||||
links?: LinksFunction;
|
||||
loader?: LoaderFunction;
|
||||
meta?: MetaFunction;
|
||||
shouldRevalidate?: ShouldRevalidateFunction;
|
||||
};
|
||||
type RSCRouteConfigEntry = RSCRouteConfigEntryBase & {
|
||||
id: string;
|
||||
path?: string;
|
||||
Component?: React.ComponentType<any>;
|
||||
lazy?: () => Promise<RSCRouteConfigEntryBase & ({
|
||||
default?: React.ComponentType<any>;
|
||||
Component?: never;
|
||||
} | {
|
||||
default?: never;
|
||||
Component?: React.ComponentType<any>;
|
||||
})>;
|
||||
} & ({
|
||||
index: true;
|
||||
} | {
|
||||
children?: RSCRouteConfigEntry[];
|
||||
});
|
||||
type RSCRouteConfig = Array<RSCRouteConfigEntry>;
|
||||
type RSCRouteManifest = {
|
||||
clientAction?: ClientActionFunction;
|
||||
clientLoader?: ClientLoaderFunction;
|
||||
element?: React.ReactElement | false;
|
||||
errorElement?: React.ReactElement;
|
||||
handle?: any;
|
||||
hasAction: boolean;
|
||||
hasComponent: boolean;
|
||||
hasErrorBoundary: boolean;
|
||||
hasLoader: boolean;
|
||||
hydrateFallbackElement?: React.ReactElement;
|
||||
id: string;
|
||||
index?: boolean;
|
||||
links?: LinksFunction;
|
||||
meta?: MetaFunction;
|
||||
parentId?: string;
|
||||
path?: string;
|
||||
shouldRevalidate?: ShouldRevalidateFunction;
|
||||
};
|
||||
type RSCRouteMatch = RSCRouteManifest & {
|
||||
params: Params;
|
||||
pathname: string;
|
||||
pathnameBase: string;
|
||||
};
|
||||
type RSCRenderPayload = {
|
||||
type: "render";
|
||||
actionData: Record<string, any> | null;
|
||||
basename: string | undefined;
|
||||
errors: Record<string, any> | null;
|
||||
loaderData: Record<string, any>;
|
||||
location: Location;
|
||||
routeDiscovery: RouteDiscovery;
|
||||
matches: RSCRouteMatch[];
|
||||
patches?: Promise<RSCRouteManifest[]>;
|
||||
nonce?: string;
|
||||
formState?: unknown;
|
||||
};
|
||||
type RSCManifestPayload = {
|
||||
type: "manifest";
|
||||
patches: Promise<RSCRouteManifest[]>;
|
||||
};
|
||||
type RSCActionPayload = {
|
||||
type: "action";
|
||||
actionResult: Promise<unknown>;
|
||||
rerender?: Promise<RSCRenderPayload | RSCRedirectPayload>;
|
||||
};
|
||||
type RSCRedirectPayload = {
|
||||
type: "redirect";
|
||||
status: number;
|
||||
location: string;
|
||||
replace: boolean;
|
||||
reload: boolean;
|
||||
actionResult?: Promise<unknown>;
|
||||
};
|
||||
type RSCPayload = RSCRenderPayload | RSCManifestPayload | RSCActionPayload | RSCRedirectPayload;
|
||||
type RSCMatch = {
|
||||
statusCode: number;
|
||||
headers: Headers;
|
||||
payload: RSCPayload;
|
||||
};
|
||||
type DecodeActionFunction = (formData: FormData) => Promise<() => Promise<unknown>>;
|
||||
type DecodeFormStateFunction = (result: unknown, formData: FormData) => unknown;
|
||||
type DecodeReplyFunction = (reply: FormData | string, options: {
|
||||
temporaryReferences: unknown;
|
||||
}) => Promise<unknown[]>;
|
||||
type LoadServerActionFunction = (id: string) => Promise<Function>;
|
||||
type RouteDiscovery = {
|
||||
mode: "lazy";
|
||||
manifestPath?: string | undefined;
|
||||
} | {
|
||||
mode: "initial";
|
||||
};
|
||||
/**
|
||||
* Matches the given routes to a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
|
||||
* and returns an [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
|
||||
* encoding an {@link unstable_RSCPayload} for consumption by an [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* enabled client router.
|
||||
*
|
||||
* @example
|
||||
* import {
|
||||
* createTemporaryReferenceSet,
|
||||
* decodeAction,
|
||||
* decodeReply,
|
||||
* loadServerAction,
|
||||
* renderToReadableStream,
|
||||
* } from "@vitejs/plugin-rsc/rsc";
|
||||
* import { unstable_matchRSCServerRequest as matchRSCServerRequest } from "react-router";
|
||||
*
|
||||
* matchRSCServerRequest({
|
||||
* createTemporaryReferenceSet,
|
||||
* decodeAction,
|
||||
* decodeFormState,
|
||||
* decodeReply,
|
||||
* loadServerAction,
|
||||
* request,
|
||||
* routes: routes(),
|
||||
* generateResponse(match) {
|
||||
* return new Response(
|
||||
* renderToReadableStream(match.payload),
|
||||
* {
|
||||
* status: match.statusCode,
|
||||
* headers: match.headers,
|
||||
* }
|
||||
* );
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* @name unstable_matchRSCServerRequest
|
||||
* @public
|
||||
* @category RSC
|
||||
* @mode data
|
||||
* @param opts Options
|
||||
* @param opts.allowedActionOrigins Origin patterns that are allowed to execute actions.
|
||||
* @param opts.basename The basename to use when matching the request.
|
||||
* @param opts.createTemporaryReferenceSet A function that returns a temporary
|
||||
* reference set for the request, used to track temporary references in the [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* stream.
|
||||
* @param opts.decodeAction Your `react-server-dom-xyz/server`'s `decodeAction`
|
||||
* function, responsible for loading a server action.
|
||||
* @param opts.decodeFormState A function responsible for decoding form state for
|
||||
* progressively enhanceable forms with React's [`useActionState`](https://react.dev/reference/react/useActionState)
|
||||
* using your `react-server-dom-xyz/server`'s `decodeFormState`.
|
||||
* @param opts.decodeReply Your `react-server-dom-xyz/server`'s `decodeReply`
|
||||
* function, used to decode the server function's arguments and bind them to the
|
||||
* implementation for invocation by the router.
|
||||
* @param opts.generateResponse A function responsible for using your
|
||||
* `renderToReadableStream` to generate a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
|
||||
* encoding the {@link unstable_RSCPayload}.
|
||||
* @param opts.loadServerAction Your `react-server-dom-xyz/server`'s
|
||||
* `loadServerAction` function, used to load a server action by ID.
|
||||
* @param opts.onError An optional error handler that will be called with any
|
||||
* errors that occur during the request processing.
|
||||
* @param opts.request The [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
|
||||
* to match against.
|
||||
* @param opts.requestContext An instance of {@link RouterContextProvider}
|
||||
* that should be created per request, to be passed to [`action`](../../start/data/route-object#action)s,
|
||||
* [`loader`](../../start/data/route-object#loader)s and [middleware](../../how-to/middleware).
|
||||
* @param opts.routeDiscovery The route discovery configuration, used to determine how the router should discover new routes during navigations.
|
||||
* @param opts.routes Your {@link unstable_RSCRouteConfigEntry | route definitions}.
|
||||
* @returns A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
|
||||
* that contains the [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* data for hydration.
|
||||
*/
|
||||
declare function matchRSCServerRequest({ allowedActionOrigins, createTemporaryReferenceSet, basename, decodeReply, requestContext, routeDiscovery, loadServerAction, decodeAction, decodeFormState, onError, request, routes, generateResponse, }: {
|
||||
allowedActionOrigins?: string[];
|
||||
createTemporaryReferenceSet: () => unknown;
|
||||
basename?: string;
|
||||
decodeReply?: DecodeReplyFunction;
|
||||
decodeAction?: DecodeActionFunction;
|
||||
decodeFormState?: DecodeFormStateFunction;
|
||||
requestContext?: RouterContextProvider;
|
||||
loadServerAction?: LoadServerActionFunction;
|
||||
onError?: (error: unknown) => void;
|
||||
request: Request;
|
||||
routes: RSCRouteConfigEntry[];
|
||||
routeDiscovery?: RouteDiscovery;
|
||||
generateResponse: (match: RSCMatch, { onError, temporaryReferences, }: {
|
||||
onError(error: unknown): string | undefined;
|
||||
temporaryReferences: unknown;
|
||||
}) => Response;
|
||||
}): Promise<Response>;
|
||||
|
||||
type BrowserCreateFromReadableStreamFunction = (body: ReadableStream<Uint8Array>, { temporaryReferences, }: {
|
||||
temporaryReferences: unknown;
|
||||
}) => Promise<unknown>;
|
||||
type EncodeReplyFunction = (args: unknown[], options: {
|
||||
temporaryReferences: unknown;
|
||||
}) => Promise<BodyInit>;
|
||||
/**
|
||||
* Create a React `callServer` implementation for React Router.
|
||||
*
|
||||
* @example
|
||||
* import {
|
||||
* createFromReadableStream,
|
||||
* createTemporaryReferenceSet,
|
||||
* encodeReply,
|
||||
* setServerCallback,
|
||||
* } from "@vitejs/plugin-rsc/browser";
|
||||
* import { unstable_createCallServer as createCallServer } from "react-router";
|
||||
*
|
||||
* setServerCallback(
|
||||
* createCallServer({
|
||||
* createFromReadableStream,
|
||||
* createTemporaryReferenceSet,
|
||||
* encodeReply,
|
||||
* })
|
||||
* );
|
||||
*
|
||||
* @name unstable_createCallServer
|
||||
* @public
|
||||
* @category RSC
|
||||
* @mode data
|
||||
* @param opts Options
|
||||
* @param opts.createFromReadableStream Your `react-server-dom-xyz/client`'s
|
||||
* `createFromReadableStream`. Used to decode payloads from the server.
|
||||
* @param opts.createTemporaryReferenceSet A function that creates a temporary
|
||||
* reference set for the [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* payload.
|
||||
* @param opts.encodeReply Your `react-server-dom-xyz/client`'s `encodeReply`.
|
||||
* Used when sending payloads to the server.
|
||||
* @param opts.fetch Optional [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
|
||||
* implementation. Defaults to global [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch).
|
||||
* @returns A function that can be used to call server actions.
|
||||
*/
|
||||
declare function createCallServer({ createFromReadableStream, createTemporaryReferenceSet, encodeReply, fetch: fetchImplementation, }: {
|
||||
createFromReadableStream: BrowserCreateFromReadableStreamFunction;
|
||||
createTemporaryReferenceSet: () => unknown;
|
||||
encodeReply: EncodeReplyFunction;
|
||||
fetch?: (request: Request) => Promise<Response>;
|
||||
}): (id: string, args: unknown[]) => Promise<unknown>;
|
||||
/**
|
||||
* Props for the {@link unstable_RSCHydratedRouter} component.
|
||||
*
|
||||
* @name unstable_RSCHydratedRouterProps
|
||||
* @category Types
|
||||
*/
|
||||
interface RSCHydratedRouterProps {
|
||||
/**
|
||||
* Your `react-server-dom-xyz/client`'s `createFromReadableStream` function,
|
||||
* used to decode payloads from the server.
|
||||
*/
|
||||
createFromReadableStream: BrowserCreateFromReadableStreamFunction;
|
||||
/**
|
||||
* Optional fetch implementation. Defaults to global [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch).
|
||||
*/
|
||||
fetch?: (request: Request) => Promise<Response>;
|
||||
/**
|
||||
* The decoded {@link unstable_RSCPayload} to hydrate.
|
||||
*/
|
||||
payload: RSCPayload;
|
||||
/**
|
||||
* A function that returns an {@link RouterContextProvider} instance
|
||||
* which is provided as the `context` argument to client [`action`](../../start/data/route-object#action)s,
|
||||
* [`loader`](../../start/data/route-object#loader)s and [middleware](../../how-to/middleware).
|
||||
* This function is called to generate a fresh `context` instance on each
|
||||
* navigation or fetcher call.
|
||||
*/
|
||||
getContext?: RouterInit["getContext"];
|
||||
}
|
||||
/**
|
||||
* Hydrates a server rendered {@link unstable_RSCPayload} in the browser.
|
||||
*
|
||||
* @example
|
||||
* import { startTransition, StrictMode } from "react";
|
||||
* import { hydrateRoot } from "react-dom/client";
|
||||
* import {
|
||||
* unstable_getRSCStream as getRSCStream,
|
||||
* unstable_RSCHydratedRouter as RSCHydratedRouter,
|
||||
* } from "react-router";
|
||||
* import type { unstable_RSCPayload as RSCPayload } from "react-router";
|
||||
*
|
||||
* createFromReadableStream(getRSCStream()).then((payload) =>
|
||||
* startTransition(async () => {
|
||||
* hydrateRoot(
|
||||
* document,
|
||||
* <StrictMode>
|
||||
* <RSCHydratedRouter
|
||||
* createFromReadableStream={createFromReadableStream}
|
||||
* payload={payload}
|
||||
* />
|
||||
* </StrictMode>,
|
||||
* { formState: await getFormState(payload) },
|
||||
* );
|
||||
* }),
|
||||
* );
|
||||
*
|
||||
* @name unstable_RSCHydratedRouter
|
||||
* @public
|
||||
* @category RSC
|
||||
* @mode data
|
||||
* @param props Props
|
||||
* @param {unstable_RSCHydratedRouterProps.createFromReadableStream} props.createFromReadableStream n/a
|
||||
* @param {unstable_RSCHydratedRouterProps.fetch} props.fetch n/a
|
||||
* @param {unstable_RSCHydratedRouterProps.getContext} props.getContext n/a
|
||||
* @param {unstable_RSCHydratedRouterProps.payload} props.payload n/a
|
||||
* @returns A hydrated {@link DataRouter} that can be used to navigate and
|
||||
* render routes.
|
||||
*/
|
||||
declare function RSCHydratedRouter({ createFromReadableStream, fetch: fetchImplementation, payload, getContext, }: RSCHydratedRouterProps): React.JSX.Element;
|
||||
|
||||
export { type BrowserCreateFromReadableStreamFunction as B, type DecodeActionFunction as D, type EncodeReplyFunction as E, type LoadServerActionFunction as L, RSCHydratedRouter as R, type DecodeFormStateFunction as a, type DecodeReplyFunction as b, createCallServer as c, type RSCManifestPayload as d, type RSCPayload as e, type RSCRenderPayload as f, getRequest as g, type RSCHydratedRouterProps as h, type RSCMatch as i, type RSCRouteManifest as j, type RSCRouteMatch as k, type RSCRouteConfigEntry as l, matchRSCServerRequest as m, type RSCRouteConfig as n };
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
import * as React from 'react';
|
||||
import { R as RouterInit } from './context-CeD5LmaF.mjs';
|
||||
import { L as Location, C as ClientActionFunction, a as ClientLoaderFunction, b as LinksFunction, M as MetaFunction, S as ShouldRevalidateFunction, P as Params, c as RouterContextProvider, A as ActionFunction, H as HeadersFunction, d as LoaderFunction } from './data-DEjBmEfD.mjs';
|
||||
|
||||
declare function getRequest(): Request;
|
||||
type RSCRouteConfigEntryBase = {
|
||||
action?: ActionFunction;
|
||||
clientAction?: ClientActionFunction;
|
||||
clientLoader?: ClientLoaderFunction;
|
||||
ErrorBoundary?: React.ComponentType<any>;
|
||||
handle?: any;
|
||||
headers?: HeadersFunction;
|
||||
HydrateFallback?: React.ComponentType<any>;
|
||||
Layout?: React.ComponentType<any>;
|
||||
links?: LinksFunction;
|
||||
loader?: LoaderFunction;
|
||||
meta?: MetaFunction;
|
||||
shouldRevalidate?: ShouldRevalidateFunction;
|
||||
};
|
||||
type RSCRouteConfigEntry = RSCRouteConfigEntryBase & {
|
||||
id: string;
|
||||
path?: string;
|
||||
Component?: React.ComponentType<any>;
|
||||
lazy?: () => Promise<RSCRouteConfigEntryBase & ({
|
||||
default?: React.ComponentType<any>;
|
||||
Component?: never;
|
||||
} | {
|
||||
default?: never;
|
||||
Component?: React.ComponentType<any>;
|
||||
})>;
|
||||
} & ({
|
||||
index: true;
|
||||
} | {
|
||||
children?: RSCRouteConfigEntry[];
|
||||
});
|
||||
type RSCRouteConfig = Array<RSCRouteConfigEntry>;
|
||||
type RSCRouteManifest = {
|
||||
clientAction?: ClientActionFunction;
|
||||
clientLoader?: ClientLoaderFunction;
|
||||
element?: React.ReactElement | false;
|
||||
errorElement?: React.ReactElement;
|
||||
handle?: any;
|
||||
hasAction: boolean;
|
||||
hasComponent: boolean;
|
||||
hasErrorBoundary: boolean;
|
||||
hasLoader: boolean;
|
||||
hydrateFallbackElement?: React.ReactElement;
|
||||
id: string;
|
||||
index?: boolean;
|
||||
links?: LinksFunction;
|
||||
meta?: MetaFunction;
|
||||
parentId?: string;
|
||||
path?: string;
|
||||
shouldRevalidate?: ShouldRevalidateFunction;
|
||||
};
|
||||
type RSCRouteMatch = RSCRouteManifest & {
|
||||
params: Params;
|
||||
pathname: string;
|
||||
pathnameBase: string;
|
||||
};
|
||||
type RSCRenderPayload = {
|
||||
type: "render";
|
||||
actionData: Record<string, any> | null;
|
||||
basename: string | undefined;
|
||||
errors: Record<string, any> | null;
|
||||
loaderData: Record<string, any>;
|
||||
location: Location;
|
||||
routeDiscovery: RouteDiscovery;
|
||||
matches: RSCRouteMatch[];
|
||||
patches?: Promise<RSCRouteManifest[]>;
|
||||
nonce?: string;
|
||||
formState?: unknown;
|
||||
};
|
||||
type RSCManifestPayload = {
|
||||
type: "manifest";
|
||||
patches: Promise<RSCRouteManifest[]>;
|
||||
};
|
||||
type RSCActionPayload = {
|
||||
type: "action";
|
||||
actionResult: Promise<unknown>;
|
||||
rerender?: Promise<RSCRenderPayload | RSCRedirectPayload>;
|
||||
};
|
||||
type RSCRedirectPayload = {
|
||||
type: "redirect";
|
||||
status: number;
|
||||
location: string;
|
||||
replace: boolean;
|
||||
reload: boolean;
|
||||
actionResult?: Promise<unknown>;
|
||||
};
|
||||
type RSCPayload = RSCRenderPayload | RSCManifestPayload | RSCActionPayload | RSCRedirectPayload;
|
||||
type RSCMatch = {
|
||||
statusCode: number;
|
||||
headers: Headers;
|
||||
payload: RSCPayload;
|
||||
};
|
||||
type DecodeActionFunction = (formData: FormData) => Promise<() => Promise<unknown>>;
|
||||
type DecodeFormStateFunction = (result: unknown, formData: FormData) => unknown;
|
||||
type DecodeReplyFunction = (reply: FormData | string, options: {
|
||||
temporaryReferences: unknown;
|
||||
}) => Promise<unknown[]>;
|
||||
type LoadServerActionFunction = (id: string) => Promise<Function>;
|
||||
type RouteDiscovery = {
|
||||
mode: "lazy";
|
||||
manifestPath?: string | undefined;
|
||||
} | {
|
||||
mode: "initial";
|
||||
};
|
||||
/**
|
||||
* Matches the given routes to a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
|
||||
* and returns an [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
|
||||
* encoding an {@link unstable_RSCPayload} for consumption by an [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* enabled client router.
|
||||
*
|
||||
* @example
|
||||
* import {
|
||||
* createTemporaryReferenceSet,
|
||||
* decodeAction,
|
||||
* decodeReply,
|
||||
* loadServerAction,
|
||||
* renderToReadableStream,
|
||||
* } from "@vitejs/plugin-rsc/rsc";
|
||||
* import { unstable_matchRSCServerRequest as matchRSCServerRequest } from "react-router";
|
||||
*
|
||||
* matchRSCServerRequest({
|
||||
* createTemporaryReferenceSet,
|
||||
* decodeAction,
|
||||
* decodeFormState,
|
||||
* decodeReply,
|
||||
* loadServerAction,
|
||||
* request,
|
||||
* routes: routes(),
|
||||
* generateResponse(match) {
|
||||
* return new Response(
|
||||
* renderToReadableStream(match.payload),
|
||||
* {
|
||||
* status: match.statusCode,
|
||||
* headers: match.headers,
|
||||
* }
|
||||
* );
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* @name unstable_matchRSCServerRequest
|
||||
* @public
|
||||
* @category RSC
|
||||
* @mode data
|
||||
* @param opts Options
|
||||
* @param opts.allowedActionOrigins Origin patterns that are allowed to execute actions.
|
||||
* @param opts.basename The basename to use when matching the request.
|
||||
* @param opts.createTemporaryReferenceSet A function that returns a temporary
|
||||
* reference set for the request, used to track temporary references in the [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* stream.
|
||||
* @param opts.decodeAction Your `react-server-dom-xyz/server`'s `decodeAction`
|
||||
* function, responsible for loading a server action.
|
||||
* @param opts.decodeFormState A function responsible for decoding form state for
|
||||
* progressively enhanceable forms with React's [`useActionState`](https://react.dev/reference/react/useActionState)
|
||||
* using your `react-server-dom-xyz/server`'s `decodeFormState`.
|
||||
* @param opts.decodeReply Your `react-server-dom-xyz/server`'s `decodeReply`
|
||||
* function, used to decode the server function's arguments and bind them to the
|
||||
* implementation for invocation by the router.
|
||||
* @param opts.generateResponse A function responsible for using your
|
||||
* `renderToReadableStream` to generate a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
|
||||
* encoding the {@link unstable_RSCPayload}.
|
||||
* @param opts.loadServerAction Your `react-server-dom-xyz/server`'s
|
||||
* `loadServerAction` function, used to load a server action by ID.
|
||||
* @param opts.onError An optional error handler that will be called with any
|
||||
* errors that occur during the request processing.
|
||||
* @param opts.request The [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
|
||||
* to match against.
|
||||
* @param opts.requestContext An instance of {@link RouterContextProvider}
|
||||
* that should be created per request, to be passed to [`action`](../../start/data/route-object#action)s,
|
||||
* [`loader`](../../start/data/route-object#loader)s and [middleware](../../how-to/middleware).
|
||||
* @param opts.routeDiscovery The route discovery configuration, used to determine how the router should discover new routes during navigations.
|
||||
* @param opts.routes Your {@link unstable_RSCRouteConfigEntry | route definitions}.
|
||||
* @returns A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
|
||||
* that contains the [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* data for hydration.
|
||||
*/
|
||||
declare function matchRSCServerRequest({ allowedActionOrigins, createTemporaryReferenceSet, basename, decodeReply, requestContext, routeDiscovery, loadServerAction, decodeAction, decodeFormState, onError, request, routes, generateResponse, }: {
|
||||
allowedActionOrigins?: string[];
|
||||
createTemporaryReferenceSet: () => unknown;
|
||||
basename?: string;
|
||||
decodeReply?: DecodeReplyFunction;
|
||||
decodeAction?: DecodeActionFunction;
|
||||
decodeFormState?: DecodeFormStateFunction;
|
||||
requestContext?: RouterContextProvider;
|
||||
loadServerAction?: LoadServerActionFunction;
|
||||
onError?: (error: unknown) => void;
|
||||
request: Request;
|
||||
routes: RSCRouteConfigEntry[];
|
||||
routeDiscovery?: RouteDiscovery;
|
||||
generateResponse: (match: RSCMatch, { onError, temporaryReferences, }: {
|
||||
onError(error: unknown): string | undefined;
|
||||
temporaryReferences: unknown;
|
||||
}) => Response;
|
||||
}): Promise<Response>;
|
||||
|
||||
type BrowserCreateFromReadableStreamFunction = (body: ReadableStream<Uint8Array>, { temporaryReferences, }: {
|
||||
temporaryReferences: unknown;
|
||||
}) => Promise<unknown>;
|
||||
type EncodeReplyFunction = (args: unknown[], options: {
|
||||
temporaryReferences: unknown;
|
||||
}) => Promise<BodyInit>;
|
||||
/**
|
||||
* Create a React `callServer` implementation for React Router.
|
||||
*
|
||||
* @example
|
||||
* import {
|
||||
* createFromReadableStream,
|
||||
* createTemporaryReferenceSet,
|
||||
* encodeReply,
|
||||
* setServerCallback,
|
||||
* } from "@vitejs/plugin-rsc/browser";
|
||||
* import { unstable_createCallServer as createCallServer } from "react-router";
|
||||
*
|
||||
* setServerCallback(
|
||||
* createCallServer({
|
||||
* createFromReadableStream,
|
||||
* createTemporaryReferenceSet,
|
||||
* encodeReply,
|
||||
* })
|
||||
* );
|
||||
*
|
||||
* @name unstable_createCallServer
|
||||
* @public
|
||||
* @category RSC
|
||||
* @mode data
|
||||
* @param opts Options
|
||||
* @param opts.createFromReadableStream Your `react-server-dom-xyz/client`'s
|
||||
* `createFromReadableStream`. Used to decode payloads from the server.
|
||||
* @param opts.createTemporaryReferenceSet A function that creates a temporary
|
||||
* reference set for the [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* payload.
|
||||
* @param opts.encodeReply Your `react-server-dom-xyz/client`'s `encodeReply`.
|
||||
* Used when sending payloads to the server.
|
||||
* @param opts.fetch Optional [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
|
||||
* implementation. Defaults to global [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch).
|
||||
* @returns A function that can be used to call server actions.
|
||||
*/
|
||||
declare function createCallServer({ createFromReadableStream, createTemporaryReferenceSet, encodeReply, fetch: fetchImplementation, }: {
|
||||
createFromReadableStream: BrowserCreateFromReadableStreamFunction;
|
||||
createTemporaryReferenceSet: () => unknown;
|
||||
encodeReply: EncodeReplyFunction;
|
||||
fetch?: (request: Request) => Promise<Response>;
|
||||
}): (id: string, args: unknown[]) => Promise<unknown>;
|
||||
/**
|
||||
* Props for the {@link unstable_RSCHydratedRouter} component.
|
||||
*
|
||||
* @name unstable_RSCHydratedRouterProps
|
||||
* @category Types
|
||||
*/
|
||||
interface RSCHydratedRouterProps {
|
||||
/**
|
||||
* Your `react-server-dom-xyz/client`'s `createFromReadableStream` function,
|
||||
* used to decode payloads from the server.
|
||||
*/
|
||||
createFromReadableStream: BrowserCreateFromReadableStreamFunction;
|
||||
/**
|
||||
* Optional fetch implementation. Defaults to global [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch).
|
||||
*/
|
||||
fetch?: (request: Request) => Promise<Response>;
|
||||
/**
|
||||
* The decoded {@link unstable_RSCPayload} to hydrate.
|
||||
*/
|
||||
payload: RSCPayload;
|
||||
/**
|
||||
* A function that returns an {@link RouterContextProvider} instance
|
||||
* which is provided as the `context` argument to client [`action`](../../start/data/route-object#action)s,
|
||||
* [`loader`](../../start/data/route-object#loader)s and [middleware](../../how-to/middleware).
|
||||
* This function is called to generate a fresh `context` instance on each
|
||||
* navigation or fetcher call.
|
||||
*/
|
||||
getContext?: RouterInit["getContext"];
|
||||
}
|
||||
/**
|
||||
* Hydrates a server rendered {@link unstable_RSCPayload} in the browser.
|
||||
*
|
||||
* @example
|
||||
* import { startTransition, StrictMode } from "react";
|
||||
* import { hydrateRoot } from "react-dom/client";
|
||||
* import {
|
||||
* unstable_getRSCStream as getRSCStream,
|
||||
* unstable_RSCHydratedRouter as RSCHydratedRouter,
|
||||
* } from "react-router";
|
||||
* import type { unstable_RSCPayload as RSCPayload } from "react-router";
|
||||
*
|
||||
* createFromReadableStream(getRSCStream()).then((payload) =>
|
||||
* startTransition(async () => {
|
||||
* hydrateRoot(
|
||||
* document,
|
||||
* <StrictMode>
|
||||
* <RSCHydratedRouter
|
||||
* createFromReadableStream={createFromReadableStream}
|
||||
* payload={payload}
|
||||
* />
|
||||
* </StrictMode>,
|
||||
* { formState: await getFormState(payload) },
|
||||
* );
|
||||
* }),
|
||||
* );
|
||||
*
|
||||
* @name unstable_RSCHydratedRouter
|
||||
* @public
|
||||
* @category RSC
|
||||
* @mode data
|
||||
* @param props Props
|
||||
* @param {unstable_RSCHydratedRouterProps.createFromReadableStream} props.createFromReadableStream n/a
|
||||
* @param {unstable_RSCHydratedRouterProps.fetch} props.fetch n/a
|
||||
* @param {unstable_RSCHydratedRouterProps.getContext} props.getContext n/a
|
||||
* @param {unstable_RSCHydratedRouterProps.payload} props.payload n/a
|
||||
* @returns A hydrated {@link DataRouter} that can be used to navigate and
|
||||
* render routes.
|
||||
*/
|
||||
declare function RSCHydratedRouter({ createFromReadableStream, fetch: fetchImplementation, payload, getContext, }: RSCHydratedRouterProps): React.JSX.Element;
|
||||
|
||||
export { type BrowserCreateFromReadableStreamFunction as B, type DecodeActionFunction as D, type EncodeReplyFunction as E, type LoadServerActionFunction as L, RSCHydratedRouter as R, type DecodeFormStateFunction as a, type DecodeReplyFunction as b, createCallServer as c, type RSCManifestPayload as d, type RSCPayload as e, type RSCRenderPayload as f, getRequest as g, type RSCHydratedRouterProps as h, type RSCMatch as i, type RSCRouteManifest as j, type RSCRouteMatch as k, type RSCRouteConfigEntry as l, matchRSCServerRequest as m, type RSCRouteConfig as n };
|
||||
+11606
File diff suppressed because it is too large
Load Diff
+2517
File diff suppressed because it is too large
Load Diff
+10307
File diff suppressed because one or more lines are too long
+188
@@ -0,0 +1,188 @@
|
||||
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }/**
|
||||
* react-router v7.18.0
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var _chunkU7ORXROYjs = require('./chunk-U7ORXROY.js');
|
||||
|
||||
// lib/dom/ssr/hydration.tsx
|
||||
function getHydrationData({
|
||||
state,
|
||||
routes,
|
||||
getRouteInfo,
|
||||
location,
|
||||
basename,
|
||||
isSpaMode
|
||||
}) {
|
||||
let hydrationData = {
|
||||
...state,
|
||||
loaderData: { ...state.loaderData }
|
||||
};
|
||||
let initialMatches = _chunkU7ORXROYjs.matchRoutes.call(void 0, routes, location, basename);
|
||||
if (initialMatches) {
|
||||
for (let match of initialMatches) {
|
||||
let routeId = match.route.id;
|
||||
let routeInfo = getRouteInfo(routeId);
|
||||
if (_chunkU7ORXROYjs.shouldHydrateRouteLoader.call(void 0,
|
||||
routeId,
|
||||
routeInfo.clientLoader,
|
||||
routeInfo.hasLoader,
|
||||
isSpaMode
|
||||
) && (routeInfo.hasHydrateFallback || !routeInfo.hasLoader)) {
|
||||
delete hydrationData.loaderData[routeId];
|
||||
} else if (!routeInfo.hasLoader) {
|
||||
hydrationData.loaderData[routeId] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return hydrationData;
|
||||
}
|
||||
|
||||
// lib/rsc/errorBoundaries.tsx
|
||||
var _react = require('react'); var _react2 = _interopRequireDefault(_react);
|
||||
var RSCRouterGlobalErrorBoundary = class extends _react2.default.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { error: null, location: props.location };
|
||||
}
|
||||
static getDerivedStateFromError(error) {
|
||||
return { error };
|
||||
}
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
if (state.location !== props.location) {
|
||||
return { error: null, location: props.location };
|
||||
}
|
||||
return { error: state.error, location: state.location };
|
||||
}
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return /* @__PURE__ */ _react2.default.createElement(
|
||||
RSCDefaultRootErrorBoundaryImpl,
|
||||
{
|
||||
error: this.state.error,
|
||||
renderAppShell: true
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
};
|
||||
function ErrorWrapper({
|
||||
renderAppShell,
|
||||
title,
|
||||
children
|
||||
}) {
|
||||
if (!renderAppShell) {
|
||||
return children;
|
||||
}
|
||||
return /* @__PURE__ */ _react2.default.createElement("html", { lang: "en" }, /* @__PURE__ */ _react2.default.createElement("head", null, /* @__PURE__ */ _react2.default.createElement("meta", { charSet: "utf-8" }), /* @__PURE__ */ _react2.default.createElement(
|
||||
"meta",
|
||||
{
|
||||
name: "viewport",
|
||||
content: "width=device-width,initial-scale=1,viewport-fit=cover"
|
||||
}
|
||||
), /* @__PURE__ */ _react2.default.createElement("title", null, title)), /* @__PURE__ */ _react2.default.createElement("body", null, /* @__PURE__ */ _react2.default.createElement("main", { style: { fontFamily: "system-ui, sans-serif", padding: "2rem" } }, children)));
|
||||
}
|
||||
function RSCDefaultRootErrorBoundaryImpl({
|
||||
error,
|
||||
renderAppShell
|
||||
}) {
|
||||
console.error(error);
|
||||
let heyDeveloper = /* @__PURE__ */ _react2.default.createElement(
|
||||
"script",
|
||||
{
|
||||
dangerouslySetInnerHTML: {
|
||||
__html: `
|
||||
console.log(
|
||||
"\u{1F4BF} Hey developer \u{1F44B}. You can provide a way better UX than this when your app throws errors. Check out https://reactrouter.com/how-to/error-boundary for more information."
|
||||
);
|
||||
`
|
||||
}
|
||||
}
|
||||
);
|
||||
if (_chunkU7ORXROYjs.isRouteErrorResponse.call(void 0, error)) {
|
||||
return /* @__PURE__ */ _react2.default.createElement(
|
||||
ErrorWrapper,
|
||||
{
|
||||
renderAppShell,
|
||||
title: "Unhandled Thrown Response!"
|
||||
},
|
||||
/* @__PURE__ */ _react2.default.createElement("h1", { style: { fontSize: "24px" } }, error.status, " ", error.statusText),
|
||||
_chunkU7ORXROYjs.ENABLE_DEV_WARNINGS ? heyDeveloper : null
|
||||
);
|
||||
}
|
||||
let errorInstance;
|
||||
if (error instanceof Error) {
|
||||
errorInstance = error;
|
||||
} else {
|
||||
let errorString = error == null ? "Unknown Error" : typeof error === "object" && "toString" in error ? error.toString() : JSON.stringify(error);
|
||||
errorInstance = new Error(errorString);
|
||||
}
|
||||
return /* @__PURE__ */ _react2.default.createElement(ErrorWrapper, { renderAppShell, title: "Application Error!" }, /* @__PURE__ */ _react2.default.createElement("h1", { style: { fontSize: "24px" } }, "Application Error"), /* @__PURE__ */ _react2.default.createElement(
|
||||
"pre",
|
||||
{
|
||||
style: {
|
||||
padding: "2rem",
|
||||
background: "hsla(10, 50%, 50%, 0.1)",
|
||||
color: "red",
|
||||
overflow: "auto"
|
||||
}
|
||||
},
|
||||
errorInstance.stack
|
||||
), heyDeveloper);
|
||||
}
|
||||
function RSCDefaultRootErrorBoundary({
|
||||
hasRootLayout
|
||||
}) {
|
||||
let error = _chunkU7ORXROYjs.useRouteError.call(void 0, );
|
||||
if (hasRootLayout === void 0) {
|
||||
throw new Error("Missing 'hasRootLayout' prop");
|
||||
}
|
||||
return /* @__PURE__ */ _react2.default.createElement(
|
||||
RSCDefaultRootErrorBoundaryImpl,
|
||||
{
|
||||
renderAppShell: !hasRootLayout,
|
||||
error
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// lib/rsc/route-modules.ts
|
||||
function createRSCRouteModules(payload) {
|
||||
const routeModules = {};
|
||||
for (const match of payload.matches) {
|
||||
populateRSCRouteModules(routeModules, match);
|
||||
}
|
||||
return routeModules;
|
||||
}
|
||||
function populateRSCRouteModules(routeModules, matches) {
|
||||
matches = Array.isArray(matches) ? matches : [matches];
|
||||
for (const match of matches) {
|
||||
routeModules[match.id] = {
|
||||
links: match.links,
|
||||
meta: match.meta,
|
||||
default: noopComponent
|
||||
};
|
||||
}
|
||||
}
|
||||
var noopComponent = () => null;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
exports.getHydrationData = getHydrationData; exports.RSCRouterGlobalErrorBoundary = RSCRouterGlobalErrorBoundary; exports.RSCDefaultRootErrorBoundary = RSCDefaultRootErrorBoundary; exports.createRSCRouteModules = createRSCRouteModules; exports.populateRSCRouteModules = populateRSCRouteModules;
|
||||
+1366
File diff suppressed because it is too large
Load Diff
+1779
File diff suppressed because it is too large
Load Diff
+1740
File diff suppressed because it is too large
Load Diff
+1740
File diff suppressed because it is too large
Load Diff
+172
@@ -0,0 +1,172 @@
|
||||
import * as React from 'react';
|
||||
import { a as RouterProviderProps$1, R as RouterInit, C as ClientInstrumentation, b as ClientOnErrorFunction } from './context-CeD5LmaF.mjs';
|
||||
export { D as unstable_DecodeActionFunction, a as unstable_DecodeFormStateFunction, b as unstable_DecodeReplyFunction, R as unstable_RSCHydratedRouter, d as unstable_RSCManifestPayload, e as unstable_RSCPayload, f as unstable_RSCRenderPayload, c as unstable_createCallServer } from './browser-DBmQ1yAR.mjs';
|
||||
import './data-DEjBmEfD.mjs';
|
||||
|
||||
type RouterProviderProps = Omit<RouterProviderProps$1, "flushSync">;
|
||||
declare function RouterProvider(props: RouterProviderProps): React.JSX.Element;
|
||||
|
||||
/**
|
||||
* Props for the {@link dom.HydratedRouter} component.
|
||||
*
|
||||
* @category Types
|
||||
*/
|
||||
interface HydratedRouterProps {
|
||||
/**
|
||||
* Context factory function to be passed through to {@link createBrowserRouter}.
|
||||
* This function will be called to create a fresh `context` instance on each
|
||||
* navigation/fetch and made available to
|
||||
* [`clientAction`](../../start/framework/route-module#clientAction)/[`clientLoader`](../../start/framework/route-module#clientLoader)
|
||||
* functions.
|
||||
*/
|
||||
getContext?: RouterInit["getContext"];
|
||||
/**
|
||||
* Array of instrumentation objects allowing you to instrument the router and
|
||||
* individual routes prior to router initialization (and on any subsequently
|
||||
* added routes via `route.lazy` or `patchRoutesOnNavigation`). This is
|
||||
* mostly useful for observability such as wrapping navigations, fetches,
|
||||
* as well as route loaders/actions/middlewares with logging and/or performance
|
||||
* tracing. See the [docs](../../how-to/instrumentation) for more information.
|
||||
*
|
||||
* ```tsx
|
||||
* const logging = {
|
||||
* router({ instrument }) {
|
||||
* instrument({
|
||||
* navigate: (impl, { to }) => logExecution(`navigate ${to}`, impl),
|
||||
* fetch: (impl, { to }) => logExecution(`fetch ${to}`, impl)
|
||||
* });
|
||||
* },
|
||||
* route({ instrument, id }) {
|
||||
* instrument({
|
||||
* middleware: (impl, { request }) => logExecution(
|
||||
* `middleware ${request.url} (route ${id})`,
|
||||
* impl
|
||||
* ),
|
||||
* loader: (impl, { request }) => logExecution(
|
||||
* `loader ${request.url} (route ${id})`,
|
||||
* impl
|
||||
* ),
|
||||
* action: (impl, { request }) => logExecution(
|
||||
* `action ${request.url} (route ${id})`,
|
||||
* impl
|
||||
* ),
|
||||
* })
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* async function logExecution(label: string, impl: () => Promise<void>) {
|
||||
* let start = performance.now();
|
||||
* console.log(`start ${label}`);
|
||||
* await impl();
|
||||
* let duration = Math.round(performance.now() - start);
|
||||
* console.log(`end ${label} (${duration}ms)`);
|
||||
* }
|
||||
*
|
||||
* startTransition(() => {
|
||||
* hydrateRoot(
|
||||
* document,
|
||||
* <HydratedRouter instrumentations={[logging]} />
|
||||
* );
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
instrumentations?: ClientInstrumentation[];
|
||||
/**
|
||||
* An error handler function that will be called for any middleware, loader, action,
|
||||
* or render errors that are encountered in your application. This is useful for
|
||||
* logging or reporting errors instead of in the {@link ErrorBoundary} because it's not
|
||||
* subject to re-rendering and will only run one time per error.
|
||||
*
|
||||
* The `errorInfo` parameter is passed along from
|
||||
* [`componentDidCatch`](https://react.dev/reference/react/Component#componentdidcatch)
|
||||
* and is only present for render errors.
|
||||
*
|
||||
* ```tsx
|
||||
* <HydratedRouter onError={(error, info) => {
|
||||
* let { location, params, pattern, errorInfo } = info;
|
||||
* console.error(error, location, errorInfo);
|
||||
* reportToErrorService(error, location, errorInfo);
|
||||
* }} />
|
||||
* ```
|
||||
*/
|
||||
onError?: ClientOnErrorFunction;
|
||||
/**
|
||||
* Control whether router state updates are internally wrapped in
|
||||
* [`React.startTransition`](https://react.dev/reference/react/startTransition).
|
||||
*
|
||||
* - When left `undefined`, all state updates are wrapped in
|
||||
* `React.startTransition`
|
||||
* - This can lead to buggy behaviors if you are wrapping your own
|
||||
* navigations/fetchers in `startTransition`.
|
||||
* - When set to `true`, {@link Link} and {@link Form} navigations will be wrapped
|
||||
* in `React.startTransition` and router state changes will be wrapped in
|
||||
* `React.startTransition` and also sent through
|
||||
* [`useOptimistic`](https://react.dev/reference/react/useOptimistic) to
|
||||
* surface mid-navigation router state changes to the UI.
|
||||
* - When set to `false`, the router will not leverage `React.startTransition` or
|
||||
* `React.useOptimistic` on any navigations or state changes.
|
||||
*
|
||||
* For more information, please see the [docs](../../explanation/react-transitions).
|
||||
*/
|
||||
useTransitions?: boolean;
|
||||
}
|
||||
/**
|
||||
* Framework-mode router component to be used to hydrate a router from a
|
||||
* {@link ServerRouter}. See [`entry.client.tsx`](../framework-conventions/entry.client.tsx).
|
||||
*
|
||||
* @public
|
||||
* @category Framework Routers
|
||||
* @mode framework
|
||||
* @param props Props
|
||||
* @param {dom.HydratedRouterProps.getContext} props.getContext n/a
|
||||
* @param {dom.HydratedRouterProps.onError} props.onError n/a
|
||||
* @returns A React element that represents the hydrated application.
|
||||
*/
|
||||
declare function HydratedRouter(props: HydratedRouterProps): React.JSX.Element;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__FLIGHT_DATA: any[];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the prerendered [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* stream for hydration. Usually passed directly to your
|
||||
* `react-server-dom-xyz/client`'s `createFromReadableStream`.
|
||||
*
|
||||
* @example
|
||||
* import { startTransition, StrictMode } from "react";
|
||||
* import { hydrateRoot } from "react-dom/client";
|
||||
* import {
|
||||
* unstable_getRSCStream as getRSCStream,
|
||||
* unstable_RSCHydratedRouter as RSCHydratedRouter,
|
||||
* } from "react-router";
|
||||
* import type { unstable_RSCPayload as RSCPayload } from "react-router";
|
||||
*
|
||||
* createFromReadableStream(getRSCStream()).then(
|
||||
* (payload: RSCServerPayload) => {
|
||||
* startTransition(async () => {
|
||||
* hydrateRoot(
|
||||
* document,
|
||||
* <StrictMode>
|
||||
* <RSCHydratedRouter {...props} />
|
||||
* </StrictMode>,
|
||||
* {
|
||||
* // Options
|
||||
* }
|
||||
* );
|
||||
* });
|
||||
* }
|
||||
* );
|
||||
*
|
||||
* @name unstable_getRSCStream
|
||||
* @public
|
||||
* @category RSC
|
||||
* @mode data
|
||||
* @returns A [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
|
||||
* that contains the [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* data for hydration.
|
||||
*/
|
||||
declare function getRSCStream(): ReadableStream;
|
||||
|
||||
export { HydratedRouter, type HydratedRouterProps, RouterProvider, type RouterProviderProps, getRSCStream as unstable_getRSCStream };
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
import * as React from 'react';
|
||||
import { RouterProviderProps as RouterProviderProps$1, RouterInit, ClientOnErrorFunction } from 'react-router';
|
||||
import { C as ClientInstrumentation } from './instrumentation-Dkmpzd13.js';
|
||||
export { D as unstable_DecodeActionFunction, a as unstable_DecodeFormStateFunction, b as unstable_DecodeReplyFunction, R as unstable_RSCHydratedRouter, d as unstable_RSCManifestPayload, e as unstable_RSCPayload, f as unstable_RSCRenderPayload, c as unstable_createCallServer } from './browser-B2PdsXXH.js';
|
||||
import './data-CjO11-hU.js';
|
||||
|
||||
type RouterProviderProps = Omit<RouterProviderProps$1, "flushSync">;
|
||||
declare function RouterProvider(props: RouterProviderProps): React.JSX.Element;
|
||||
|
||||
/**
|
||||
* Props for the {@link dom.HydratedRouter} component.
|
||||
*
|
||||
* @category Types
|
||||
*/
|
||||
interface HydratedRouterProps {
|
||||
/**
|
||||
* Context factory function to be passed through to {@link createBrowserRouter}.
|
||||
* This function will be called to create a fresh `context` instance on each
|
||||
* navigation/fetch and made available to
|
||||
* [`clientAction`](../../start/framework/route-module#clientAction)/[`clientLoader`](../../start/framework/route-module#clientLoader)
|
||||
* functions.
|
||||
*/
|
||||
getContext?: RouterInit["getContext"];
|
||||
/**
|
||||
* Array of instrumentation objects allowing you to instrument the router and
|
||||
* individual routes prior to router initialization (and on any subsequently
|
||||
* added routes via `route.lazy` or `patchRoutesOnNavigation`). This is
|
||||
* mostly useful for observability such as wrapping navigations, fetches,
|
||||
* as well as route loaders/actions/middlewares with logging and/or performance
|
||||
* tracing. See the [docs](../../how-to/instrumentation) for more information.
|
||||
*
|
||||
* ```tsx
|
||||
* const logging = {
|
||||
* router({ instrument }) {
|
||||
* instrument({
|
||||
* navigate: (impl, { to }) => logExecution(`navigate ${to}`, impl),
|
||||
* fetch: (impl, { to }) => logExecution(`fetch ${to}`, impl)
|
||||
* });
|
||||
* },
|
||||
* route({ instrument, id }) {
|
||||
* instrument({
|
||||
* middleware: (impl, { request }) => logExecution(
|
||||
* `middleware ${request.url} (route ${id})`,
|
||||
* impl
|
||||
* ),
|
||||
* loader: (impl, { request }) => logExecution(
|
||||
* `loader ${request.url} (route ${id})`,
|
||||
* impl
|
||||
* ),
|
||||
* action: (impl, { request }) => logExecution(
|
||||
* `action ${request.url} (route ${id})`,
|
||||
* impl
|
||||
* ),
|
||||
* })
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* async function logExecution(label: string, impl: () => Promise<void>) {
|
||||
* let start = performance.now();
|
||||
* console.log(`start ${label}`);
|
||||
* await impl();
|
||||
* let duration = Math.round(performance.now() - start);
|
||||
* console.log(`end ${label} (${duration}ms)`);
|
||||
* }
|
||||
*
|
||||
* startTransition(() => {
|
||||
* hydrateRoot(
|
||||
* document,
|
||||
* <HydratedRouter instrumentations={[logging]} />
|
||||
* );
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
instrumentations?: ClientInstrumentation[];
|
||||
/**
|
||||
* An error handler function that will be called for any middleware, loader, action,
|
||||
* or render errors that are encountered in your application. This is useful for
|
||||
* logging or reporting errors instead of in the {@link ErrorBoundary} because it's not
|
||||
* subject to re-rendering and will only run one time per error.
|
||||
*
|
||||
* The `errorInfo` parameter is passed along from
|
||||
* [`componentDidCatch`](https://react.dev/reference/react/Component#componentdidcatch)
|
||||
* and is only present for render errors.
|
||||
*
|
||||
* ```tsx
|
||||
* <HydratedRouter onError={(error, info) => {
|
||||
* let { location, params, pattern, errorInfo } = info;
|
||||
* console.error(error, location, errorInfo);
|
||||
* reportToErrorService(error, location, errorInfo);
|
||||
* }} />
|
||||
* ```
|
||||
*/
|
||||
onError?: ClientOnErrorFunction;
|
||||
/**
|
||||
* Control whether router state updates are internally wrapped in
|
||||
* [`React.startTransition`](https://react.dev/reference/react/startTransition).
|
||||
*
|
||||
* - When left `undefined`, all state updates are wrapped in
|
||||
* `React.startTransition`
|
||||
* - This can lead to buggy behaviors if you are wrapping your own
|
||||
* navigations/fetchers in `startTransition`.
|
||||
* - When set to `true`, {@link Link} and {@link Form} navigations will be wrapped
|
||||
* in `React.startTransition` and router state changes will be wrapped in
|
||||
* `React.startTransition` and also sent through
|
||||
* [`useOptimistic`](https://react.dev/reference/react/useOptimistic) to
|
||||
* surface mid-navigation router state changes to the UI.
|
||||
* - When set to `false`, the router will not leverage `React.startTransition` or
|
||||
* `React.useOptimistic` on any navigations or state changes.
|
||||
*
|
||||
* For more information, please see the [docs](../../explanation/react-transitions).
|
||||
*/
|
||||
useTransitions?: boolean;
|
||||
}
|
||||
/**
|
||||
* Framework-mode router component to be used to hydrate a router from a
|
||||
* {@link ServerRouter}. See [`entry.client.tsx`](../framework-conventions/entry.client.tsx).
|
||||
*
|
||||
* @public
|
||||
* @category Framework Routers
|
||||
* @mode framework
|
||||
* @param props Props
|
||||
* @param {dom.HydratedRouterProps.getContext} props.getContext n/a
|
||||
* @param {dom.HydratedRouterProps.onError} props.onError n/a
|
||||
* @returns A React element that represents the hydrated application.
|
||||
*/
|
||||
declare function HydratedRouter(props: HydratedRouterProps): React.JSX.Element;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__FLIGHT_DATA: any[];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the prerendered [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* stream for hydration. Usually passed directly to your
|
||||
* `react-server-dom-xyz/client`'s `createFromReadableStream`.
|
||||
*
|
||||
* @example
|
||||
* import { startTransition, StrictMode } from "react";
|
||||
* import { hydrateRoot } from "react-dom/client";
|
||||
* import {
|
||||
* unstable_getRSCStream as getRSCStream,
|
||||
* unstable_RSCHydratedRouter as RSCHydratedRouter,
|
||||
* } from "react-router";
|
||||
* import type { unstable_RSCPayload as RSCPayload } from "react-router";
|
||||
*
|
||||
* createFromReadableStream(getRSCStream()).then(
|
||||
* (payload: RSCServerPayload) => {
|
||||
* startTransition(async () => {
|
||||
* hydrateRoot(
|
||||
* document,
|
||||
* <StrictMode>
|
||||
* <RSCHydratedRouter {...props} />
|
||||
* </StrictMode>,
|
||||
* {
|
||||
* // Options
|
||||
* }
|
||||
* );
|
||||
* });
|
||||
* }
|
||||
* );
|
||||
*
|
||||
* @name unstable_getRSCStream
|
||||
* @public
|
||||
* @category RSC
|
||||
* @mode data
|
||||
* @returns A [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
|
||||
* that contains the [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* data for hydration.
|
||||
*/
|
||||
declare function getRSCStream(): ReadableStream;
|
||||
|
||||
export { HydratedRouter, type HydratedRouterProps, RouterProvider, type RouterProviderProps, getRSCStream as unstable_getRSCStream };
|
||||
+1018
File diff suppressed because it is too large
Load Diff
+1010
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+3677
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+2614
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
export { Q as MemoryRouter, T as Navigate, U as Outlet, V as Route, W as Router, X as RouterProvider, Y as Routes, A as UNSAFE_AwaitContextProvider, ab as UNSAFE_WithComponentProps, af as UNSAFE_WithErrorBoundaryProps, ad as UNSAFE_WithHydrateFallbackProps } from './context-CeD5LmaF.mjs';
|
||||
export { l as BrowserRouter, q as Form, m as HashRouter, n as Link, X as Links, W as Meta, p as NavLink, r as ScrollRestoration, T as StaticRouter, V as StaticRouterProvider, o as unstable_HistoryRouter } from './index-react-server-client-CACgcj2J.mjs';
|
||||
import './data-DEjBmEfD.mjs';
|
||||
import 'react';
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
export { W as BrowserRouter, $ as Form, X as HashRouter, Y as Link, an as Links, j as MemoryRouter, am as Meta, _ as NavLink, k as Navigate, l as Outlet, m as Route, n as Router, o as RouterProvider, p as Routes, a0 as ScrollRestoration, ak as StaticRouter, al as StaticRouterProvider, b as UNSAFE_AwaitContextProvider, aH as UNSAFE_WithComponentProps, aL as UNSAFE_WithErrorBoundaryProps, aJ as UNSAFE_WithHydrateFallbackProps, Z as unstable_HistoryRouter } from './index-react-server-client-3ykjivgQ.js';
|
||||
import './instrumentation-Dkmpzd13.js';
|
||||
import './data-CjO11-hU.js';
|
||||
import 'react';
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
"use strict";Object.defineProperty(exports, "__esModule", {value: true});/**
|
||||
* react-router v7.18.0
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
"use client";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var _chunkYL5M26XIjs = require('./chunk-YL5M26XI.js');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var _chunkU7ORXROYjs = require('./chunk-U7ORXROY.js');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
exports.BrowserRouter = _chunkYL5M26XIjs.BrowserRouter; exports.Form = _chunkYL5M26XIjs.Form; exports.HashRouter = _chunkYL5M26XIjs.HashRouter; exports.Link = _chunkYL5M26XIjs.Link; exports.Links = _chunkU7ORXROYjs.Links; exports.MemoryRouter = _chunkU7ORXROYjs.MemoryRouter; exports.Meta = _chunkU7ORXROYjs.Meta; exports.NavLink = _chunkYL5M26XIjs.NavLink; exports.Navigate = _chunkU7ORXROYjs.Navigate; exports.Outlet = _chunkU7ORXROYjs.Outlet; exports.Route = _chunkU7ORXROYjs.Route; exports.Router = _chunkU7ORXROYjs.Router; exports.RouterProvider = _chunkU7ORXROYjs.RouterProvider; exports.Routes = _chunkU7ORXROYjs.Routes; exports.ScrollRestoration = _chunkYL5M26XIjs.ScrollRestoration; exports.StaticRouter = _chunkYL5M26XIjs.StaticRouter; exports.StaticRouterProvider = _chunkYL5M26XIjs.StaticRouterProvider; exports.UNSAFE_AwaitContextProvider = _chunkU7ORXROYjs.AwaitContextProvider; exports.UNSAFE_WithComponentProps = _chunkU7ORXROYjs.WithComponentProps; exports.UNSAFE_WithErrorBoundaryProps = _chunkU7ORXROYjs.WithErrorBoundaryProps; exports.UNSAFE_WithHydrateFallbackProps = _chunkU7ORXROYjs.WithHydrateFallbackProps; exports.unstable_HistoryRouter = _chunkYL5M26XIjs.HistoryRouter;
|
||||
Generated
Vendored
+59
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* react-router v7.18.0
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
"use client";
|
||||
import {
|
||||
AwaitContextProvider,
|
||||
BrowserRouter,
|
||||
Form,
|
||||
HashRouter,
|
||||
HistoryRouter,
|
||||
Link,
|
||||
Links,
|
||||
MemoryRouter,
|
||||
Meta,
|
||||
NavLink,
|
||||
Navigate,
|
||||
Outlet,
|
||||
Route,
|
||||
Router,
|
||||
RouterProvider,
|
||||
Routes,
|
||||
ScrollRestoration,
|
||||
StaticRouter,
|
||||
StaticRouterProvider,
|
||||
WithComponentProps,
|
||||
WithErrorBoundaryProps,
|
||||
WithHydrateFallbackProps
|
||||
} from "./chunk-4ZMWKKQ3.mjs";
|
||||
export {
|
||||
BrowserRouter,
|
||||
Form,
|
||||
HashRouter,
|
||||
Link,
|
||||
Links,
|
||||
MemoryRouter,
|
||||
Meta,
|
||||
NavLink,
|
||||
Navigate,
|
||||
Outlet,
|
||||
Route,
|
||||
Router,
|
||||
RouterProvider,
|
||||
Routes,
|
||||
ScrollRestoration,
|
||||
StaticRouter,
|
||||
StaticRouterProvider,
|
||||
AwaitContextProvider as UNSAFE_AwaitContextProvider,
|
||||
WithComponentProps as UNSAFE_WithComponentProps,
|
||||
WithErrorBoundaryProps as UNSAFE_WithErrorBoundaryProps,
|
||||
WithHydrateFallbackProps as UNSAFE_WithHydrateFallbackProps,
|
||||
HistoryRouter as unstable_HistoryRouter
|
||||
};
|
||||
+2711
File diff suppressed because it is too large
Load Diff
+2711
File diff suppressed because it is too large
Load Diff
+3915
File diff suppressed because it is too large
Load Diff
+3803
File diff suppressed because it is too large
Load Diff
+1479
File diff suppressed because it is too large
Load Diff
+1479
File diff suppressed because it is too large
Load Diff
+2562
File diff suppressed because one or more lines are too long
+275
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* react-router v7.18.0
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
"use client";
|
||||
import {
|
||||
RSCDefaultRootErrorBoundary,
|
||||
RSCStaticRouter,
|
||||
ServerMode,
|
||||
ServerRouter,
|
||||
createCookie,
|
||||
createCookieSessionStorage,
|
||||
createMemorySessionStorage,
|
||||
createRequestHandler,
|
||||
createRoutesStub,
|
||||
createSession,
|
||||
createSessionStorage,
|
||||
getHydrationData,
|
||||
href,
|
||||
isCookie,
|
||||
isSession,
|
||||
routeRSCServerRequest,
|
||||
setDevServerHooks
|
||||
} from "./chunk-E4MTK73K.mjs";
|
||||
import {
|
||||
Action,
|
||||
Await,
|
||||
AwaitContextProvider,
|
||||
BrowserRouter,
|
||||
DataRouterContext,
|
||||
DataRouterStateContext,
|
||||
ErrorResponseImpl,
|
||||
FetchersContext,
|
||||
Form,
|
||||
FrameworkContext,
|
||||
HashRouter,
|
||||
HistoryRouter,
|
||||
IDLE_BLOCKER,
|
||||
IDLE_FETCHER,
|
||||
IDLE_NAVIGATION,
|
||||
Link,
|
||||
Links,
|
||||
LocationContext,
|
||||
MemoryRouter,
|
||||
Meta,
|
||||
NavLink,
|
||||
Navigate,
|
||||
NavigationContext,
|
||||
Outlet,
|
||||
PrefetchPageLinks,
|
||||
RemixErrorBoundary,
|
||||
Route,
|
||||
RouteContext,
|
||||
Router,
|
||||
RouterContextProvider,
|
||||
RouterProvider,
|
||||
Routes,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
SingleFetchRedirectSymbol,
|
||||
StaticRouter,
|
||||
StaticRouterProvider,
|
||||
ViewTransitionContext,
|
||||
WithComponentProps,
|
||||
WithErrorBoundaryProps,
|
||||
WithHydrateFallbackProps,
|
||||
createBrowserHistory,
|
||||
createBrowserRouter,
|
||||
createClientRoutes,
|
||||
createClientRoutesWithHMRRevalidationOptOut,
|
||||
createContext,
|
||||
createHashHistory,
|
||||
createHashRouter,
|
||||
createMemoryHistory,
|
||||
createMemoryRouter,
|
||||
createPath,
|
||||
createRouter,
|
||||
createRoutesFromChildren,
|
||||
createRoutesFromElements,
|
||||
createSearchParams,
|
||||
createStaticHandler2 as createStaticHandler,
|
||||
createStaticRouter,
|
||||
data,
|
||||
decodeViaTurboStream,
|
||||
generatePath,
|
||||
getPatchRoutesOnNavigationFunction,
|
||||
getTurboStreamSingleFetchDataStrategy,
|
||||
hydrationRouteProperties,
|
||||
invariant,
|
||||
isRouteErrorResponse,
|
||||
mapRouteProperties,
|
||||
matchPath,
|
||||
matchRoutes,
|
||||
parsePath,
|
||||
redirect,
|
||||
redirectDocument,
|
||||
renderMatches,
|
||||
replace,
|
||||
resolvePath,
|
||||
shouldHydrateRouteLoader,
|
||||
useActionData,
|
||||
useAsyncError,
|
||||
useAsyncValue,
|
||||
useBeforeUnload,
|
||||
useBlocker,
|
||||
useFetcher,
|
||||
useFetchers,
|
||||
useFogOFWarDiscovery,
|
||||
useFormAction,
|
||||
useHref,
|
||||
useInRouterContext,
|
||||
useLinkClickHandler,
|
||||
useLoaderData,
|
||||
useLocation,
|
||||
useMatch,
|
||||
useMatches,
|
||||
useNavigate,
|
||||
useNavigation,
|
||||
useNavigationType,
|
||||
useOutlet,
|
||||
useOutletContext,
|
||||
useParams,
|
||||
usePrompt,
|
||||
useResolvedPath,
|
||||
useRevalidator,
|
||||
useRoute,
|
||||
useRouteError,
|
||||
useRouteLoaderData,
|
||||
useRouterState,
|
||||
useRoutes,
|
||||
useScrollRestoration,
|
||||
useSearchParams,
|
||||
useSubmit,
|
||||
useViewTransitionState,
|
||||
withComponentProps,
|
||||
withErrorBoundaryProps,
|
||||
withHydrateFallbackProps
|
||||
} from "./chunk-4ZMWKKQ3.mjs";
|
||||
export {
|
||||
Await,
|
||||
BrowserRouter,
|
||||
Form,
|
||||
HashRouter,
|
||||
IDLE_BLOCKER,
|
||||
IDLE_FETCHER,
|
||||
IDLE_NAVIGATION,
|
||||
Link,
|
||||
Links,
|
||||
MemoryRouter,
|
||||
Meta,
|
||||
NavLink,
|
||||
Navigate,
|
||||
Action as NavigationType,
|
||||
Outlet,
|
||||
PrefetchPageLinks,
|
||||
Route,
|
||||
Router,
|
||||
RouterContextProvider,
|
||||
RouterProvider,
|
||||
Routes,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
ServerRouter,
|
||||
StaticRouter,
|
||||
StaticRouterProvider,
|
||||
AwaitContextProvider as UNSAFE_AwaitContextProvider,
|
||||
DataRouterContext as UNSAFE_DataRouterContext,
|
||||
DataRouterStateContext as UNSAFE_DataRouterStateContext,
|
||||
ErrorResponseImpl as UNSAFE_ErrorResponseImpl,
|
||||
FetchersContext as UNSAFE_FetchersContext,
|
||||
FrameworkContext as UNSAFE_FrameworkContext,
|
||||
LocationContext as UNSAFE_LocationContext,
|
||||
NavigationContext as UNSAFE_NavigationContext,
|
||||
RSCDefaultRootErrorBoundary as UNSAFE_RSCDefaultRootErrorBoundary,
|
||||
RemixErrorBoundary as UNSAFE_RemixErrorBoundary,
|
||||
RouteContext as UNSAFE_RouteContext,
|
||||
ServerMode as UNSAFE_ServerMode,
|
||||
SingleFetchRedirectSymbol as UNSAFE_SingleFetchRedirectSymbol,
|
||||
ViewTransitionContext as UNSAFE_ViewTransitionContext,
|
||||
WithComponentProps as UNSAFE_WithComponentProps,
|
||||
WithErrorBoundaryProps as UNSAFE_WithErrorBoundaryProps,
|
||||
WithHydrateFallbackProps as UNSAFE_WithHydrateFallbackProps,
|
||||
createBrowserHistory as UNSAFE_createBrowserHistory,
|
||||
createClientRoutes as UNSAFE_createClientRoutes,
|
||||
createClientRoutesWithHMRRevalidationOptOut as UNSAFE_createClientRoutesWithHMRRevalidationOptOut,
|
||||
createHashHistory as UNSAFE_createHashHistory,
|
||||
createMemoryHistory as UNSAFE_createMemoryHistory,
|
||||
createRouter as UNSAFE_createRouter,
|
||||
decodeViaTurboStream as UNSAFE_decodeViaTurboStream,
|
||||
getHydrationData as UNSAFE_getHydrationData,
|
||||
getPatchRoutesOnNavigationFunction as UNSAFE_getPatchRoutesOnNavigationFunction,
|
||||
getTurboStreamSingleFetchDataStrategy as UNSAFE_getTurboStreamSingleFetchDataStrategy,
|
||||
hydrationRouteProperties as UNSAFE_hydrationRouteProperties,
|
||||
invariant as UNSAFE_invariant,
|
||||
mapRouteProperties as UNSAFE_mapRouteProperties,
|
||||
shouldHydrateRouteLoader as UNSAFE_shouldHydrateRouteLoader,
|
||||
useFogOFWarDiscovery as UNSAFE_useFogOFWarDiscovery,
|
||||
useScrollRestoration as UNSAFE_useScrollRestoration,
|
||||
withComponentProps as UNSAFE_withComponentProps,
|
||||
withErrorBoundaryProps as UNSAFE_withErrorBoundaryProps,
|
||||
withHydrateFallbackProps as UNSAFE_withHydrateFallbackProps,
|
||||
createBrowserRouter,
|
||||
createContext,
|
||||
createCookie,
|
||||
createCookieSessionStorage,
|
||||
createHashRouter,
|
||||
createMemoryRouter,
|
||||
createMemorySessionStorage,
|
||||
createPath,
|
||||
createRequestHandler,
|
||||
createRoutesFromChildren,
|
||||
createRoutesFromElements,
|
||||
createRoutesStub,
|
||||
createSearchParams,
|
||||
createSession,
|
||||
createSessionStorage,
|
||||
createStaticHandler,
|
||||
createStaticRouter,
|
||||
data,
|
||||
generatePath,
|
||||
href,
|
||||
isCookie,
|
||||
isRouteErrorResponse,
|
||||
isSession,
|
||||
matchPath,
|
||||
matchRoutes,
|
||||
parsePath,
|
||||
redirect,
|
||||
redirectDocument,
|
||||
renderMatches,
|
||||
replace,
|
||||
resolvePath,
|
||||
HistoryRouter as unstable_HistoryRouter,
|
||||
RSCStaticRouter as unstable_RSCStaticRouter,
|
||||
routeRSCServerRequest as unstable_routeRSCServerRequest,
|
||||
setDevServerHooks as unstable_setDevServerHooks,
|
||||
usePrompt as unstable_usePrompt,
|
||||
useRoute as unstable_useRoute,
|
||||
useRouterState as unstable_useRouterState,
|
||||
useActionData,
|
||||
useAsyncError,
|
||||
useAsyncValue,
|
||||
useBeforeUnload,
|
||||
useBlocker,
|
||||
useFetcher,
|
||||
useFetchers,
|
||||
useFormAction,
|
||||
useHref,
|
||||
useInRouterContext,
|
||||
useLinkClickHandler,
|
||||
useLoaderData,
|
||||
useLocation,
|
||||
useMatch,
|
||||
useMatches,
|
||||
useNavigate,
|
||||
useNavigation,
|
||||
useNavigationType,
|
||||
useOutlet,
|
||||
useOutletContext,
|
||||
useParams,
|
||||
useResolvedPath,
|
||||
useRevalidator,
|
||||
useRouteError,
|
||||
useRouteLoaderData,
|
||||
useRoutes,
|
||||
useSearchParams,
|
||||
useSubmit,
|
||||
useViewTransitionState
|
||||
};
|
||||
Generated
Vendored
+715
@@ -0,0 +1,715 @@
|
||||
import { e as RouteObject, f as History, g as MaybePromise, c as RouterContextProvider, h as MapRoutePropertiesFunction, i as Action, L as Location, D as DataRouteMatch, j as Submission, k as RouteData, l as DataStrategyFunction, m as PatchRoutesOnNavigationFunction, n as DataRouteObject, o as RouteBranch, p as RouteManifest, U as UIMatch, T as To, q as HTMLFormMethod, F as FormEncType, r as Path, s as LoaderFunctionArgs, t as MiddlewareEnabled, u as AppLoadContext } from './data-CjO11-hU.js';
|
||||
|
||||
/**
|
||||
* A Router instance manages all navigation and data loading/mutations
|
||||
*/
|
||||
interface Router {
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Return the basename for the router
|
||||
*/
|
||||
get basename(): RouterInit["basename"];
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Return the future config for the router
|
||||
*/
|
||||
get future(): FutureConfig;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Return the current state of the router
|
||||
*/
|
||||
get state(): RouterState;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Return the routes for this router instance
|
||||
*/
|
||||
get routes(): DataRouteObject[];
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Return the route branches for this router instance
|
||||
*/
|
||||
get branches(): RouteBranch<DataRouteObject>[] | undefined;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Return the manifest for this router instance
|
||||
*/
|
||||
get manifest(): RouteManifest;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Return the window associated with the router
|
||||
*/
|
||||
get window(): RouterInit["window"];
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Initialize the router, including adding history listeners and kicking off
|
||||
* initial data fetches. Returns a function to cleanup listeners and abort
|
||||
* any in-progress loads
|
||||
*/
|
||||
initialize(): Router;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Subscribe to router.state updates
|
||||
*
|
||||
* @param fn function to call with the new state
|
||||
*/
|
||||
subscribe(fn: RouterSubscriber): () => void;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Enable scroll restoration behavior in the router
|
||||
*
|
||||
* @param savedScrollPositions Object that will manage positions, in case
|
||||
* it's being restored from sessionStorage
|
||||
* @param getScrollPosition Function to get the active Y scroll position
|
||||
* @param getKey Function to get the key to use for restoration
|
||||
*/
|
||||
enableScrollRestoration(savedScrollPositions: Record<string, number>, getScrollPosition: GetScrollPositionFunction, getKey?: GetScrollRestorationKeyFunction): () => void;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Navigate forward/backward in the history stack
|
||||
* @param to Delta to move in the history stack
|
||||
*/
|
||||
navigate(to: number): Promise<void>;
|
||||
/**
|
||||
* Navigate to the given path
|
||||
* @param to Path to navigate to
|
||||
* @param opts Navigation options (method, submission, etc.)
|
||||
*/
|
||||
navigate(to: To | null, opts?: RouterNavigateOptions): Promise<void>;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Trigger a fetcher load/submission
|
||||
*
|
||||
* @param key Fetcher key
|
||||
* @param routeId Route that owns the fetcher
|
||||
* @param href href to fetch
|
||||
* @param opts Fetcher options, (method, submission, etc.)
|
||||
*/
|
||||
fetch(key: string, routeId: string, href: string | null, opts?: RouterFetchOptions): Promise<void>;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Trigger a revalidation of all current route loaders and fetcher loads
|
||||
*/
|
||||
revalidate(): Promise<void>;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Utility function to create an href for the given location
|
||||
* @param location
|
||||
*/
|
||||
createHref(location: Location | URL): string;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Utility function to URL encode a destination path according to the internal
|
||||
* history implementation
|
||||
* @param to
|
||||
*/
|
||||
encodeLocation(to: To): Path;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Get/create a fetcher for the given key
|
||||
* @param key
|
||||
*/
|
||||
getFetcher<TData = any>(key: string): Fetcher<TData>;
|
||||
/**
|
||||
* @internal
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Reset the fetcher for a given key
|
||||
* @param key
|
||||
*/
|
||||
resetFetcher(key: string, opts?: {
|
||||
reason?: unknown;
|
||||
}): void;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Delete the fetcher for a given key
|
||||
* @param key
|
||||
*/
|
||||
deleteFetcher(key: string): void;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Cleanup listeners and abort any in-progress loads
|
||||
*/
|
||||
dispose(): void;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Get a navigation blocker
|
||||
* @param key The identifier for the blocker
|
||||
* @param fn The blocker function implementation
|
||||
*/
|
||||
getBlocker(key: string, fn: BlockerFunction): Blocker;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Delete a navigation blocker
|
||||
* @param key The identifier for the blocker
|
||||
*/
|
||||
deleteBlocker(key: string): void;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE DO NOT USE
|
||||
*
|
||||
* Patch additional children routes into an existing parent route
|
||||
* @param routeId The parent route id or a callback function accepting `patch`
|
||||
* to perform batch patching
|
||||
* @param children The additional children routes
|
||||
* @param unstable_allowElementMutations Allow mutation or route elements on
|
||||
* existing routes. Intended for RSC-usage
|
||||
* only.
|
||||
*/
|
||||
patchRoutes(routeId: string | null, children: RouteObject[], unstable_allowElementMutations?: boolean): void;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* HMR needs to pass in-flight route updates to React Router
|
||||
* TODO: Replace this with granular route update APIs (addRoute, updateRoute, deleteRoute)
|
||||
*/
|
||||
_internalSetRoutes(routes: RouteObject[]): void;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Cause subscribers to re-render. This is used to force a re-render.
|
||||
*/
|
||||
_internalSetStateDoNotUseOrYouWillBreakYourApp(state: Partial<RouterState>): void;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Internal fetch AbortControllers accessed by unit tests
|
||||
*/
|
||||
_internalFetchControllers: Map<string, AbortController>;
|
||||
}
|
||||
/**
|
||||
* State maintained internally by the router. During a navigation, all states
|
||||
* reflect the "old" location unless otherwise noted.
|
||||
*/
|
||||
interface RouterState {
|
||||
/**
|
||||
* The action of the most recent navigation
|
||||
*/
|
||||
historyAction: Action;
|
||||
/**
|
||||
* The current location reflected by the router
|
||||
*/
|
||||
location: Location;
|
||||
/**
|
||||
* The current set of route matches
|
||||
*/
|
||||
matches: DataRouteMatch[];
|
||||
/**
|
||||
* Tracks whether we've completed our initial data load
|
||||
*/
|
||||
initialized: boolean;
|
||||
/**
|
||||
* Tracks whether we should be rendering a HydrateFallback during hydration
|
||||
*/
|
||||
renderFallback: boolean;
|
||||
/**
|
||||
* Current scroll position we should start at for a new view
|
||||
* - number -> scroll position to restore to
|
||||
* - false -> do not restore scroll at all (used during submissions/revalidations)
|
||||
* - null -> don't have a saved position, scroll to hash or top of page
|
||||
*/
|
||||
restoreScrollPosition: number | false | null;
|
||||
/**
|
||||
* Indicate whether this navigation should skip resetting the scroll position
|
||||
* if we are unable to restore the scroll position
|
||||
*/
|
||||
preventScrollReset: boolean;
|
||||
/**
|
||||
* Tracks the state of the current navigation
|
||||
*/
|
||||
navigation: Navigation;
|
||||
/**
|
||||
* Tracks any in-progress revalidations
|
||||
*/
|
||||
revalidation: RevalidationState;
|
||||
/**
|
||||
* Data from the loaders for the current matches
|
||||
*/
|
||||
loaderData: RouteData;
|
||||
/**
|
||||
* Data from the action for the current matches
|
||||
*/
|
||||
actionData: RouteData | null;
|
||||
/**
|
||||
* Errors caught from loaders for the current matches
|
||||
*/
|
||||
errors: RouteData | null;
|
||||
/**
|
||||
* Map of current fetchers
|
||||
*/
|
||||
fetchers: Map<string, Fetcher>;
|
||||
/**
|
||||
* Map of current blockers
|
||||
*/
|
||||
blockers: Map<string, Blocker>;
|
||||
}
|
||||
/**
|
||||
* Data that can be passed into hydrate a Router from SSR
|
||||
*/
|
||||
type HydrationState = Partial<Pick<RouterState, "loaderData" | "actionData" | "errors">>;
|
||||
/**
|
||||
* Future flags to toggle new feature behavior
|
||||
*/
|
||||
interface FutureConfig {
|
||||
}
|
||||
/**
|
||||
* Initialization options for createRouter
|
||||
*/
|
||||
interface RouterInit {
|
||||
routes: RouteObject[];
|
||||
history: History;
|
||||
basename?: string;
|
||||
getContext?: () => MaybePromise<RouterContextProvider>;
|
||||
instrumentations?: ClientInstrumentation[];
|
||||
mapRouteProperties?: MapRoutePropertiesFunction;
|
||||
future?: Partial<FutureConfig>;
|
||||
hydrationRouteProperties?: string[];
|
||||
hydrationData?: HydrationState;
|
||||
window?: Window;
|
||||
dataStrategy?: DataStrategyFunction;
|
||||
patchRoutesOnNavigation?: PatchRoutesOnNavigationFunction;
|
||||
}
|
||||
/**
|
||||
* State returned from a server-side query() call
|
||||
*/
|
||||
interface StaticHandlerContext {
|
||||
basename: Router["basename"];
|
||||
location: RouterState["location"];
|
||||
matches: RouterState["matches"];
|
||||
loaderData: RouterState["loaderData"];
|
||||
actionData: RouterState["actionData"];
|
||||
errors: RouterState["errors"];
|
||||
statusCode: number;
|
||||
loaderHeaders: Record<string, Headers>;
|
||||
actionHeaders: Record<string, Headers>;
|
||||
_deepestRenderedBoundaryId?: string | null;
|
||||
}
|
||||
/**
|
||||
* A StaticHandler instance manages a singular SSR navigation/fetch event
|
||||
*/
|
||||
interface StaticHandler {
|
||||
/**
|
||||
* The set of data routes managed by this handler
|
||||
*/
|
||||
dataRoutes: DataRouteObject[];
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* The route branches derived from the data routes, used for internal route
|
||||
* matching in Framework Mode
|
||||
*/
|
||||
_internalRouteBranches: RouteBranch<DataRouteObject>[];
|
||||
/**
|
||||
* Perform a query for a given request - executing all matched route
|
||||
* loaders/actions. Used for document requests.
|
||||
*
|
||||
* @param request The request to query
|
||||
* @param opts Optional query options
|
||||
* @param opts.dataStrategy Alternate dataStrategy implementation
|
||||
* @param opts.filterMatchesToLoad Predicate function to filter which matches should be loaded
|
||||
* @param opts.generateMiddlewareResponse To enable middleware, provide a function
|
||||
* to generate a response to bubble back up the middleware chain
|
||||
* @param opts.requestContext Context object to pass to loaders/actions
|
||||
* @param opts.skipLoaderErrorBubbling Skip loader error bubbling
|
||||
* @param opts.skipRevalidation Skip revalidation after action submission
|
||||
* @param opts.normalizePath Normalize the request path
|
||||
*/
|
||||
query(request: Request, opts?: {
|
||||
requestContext?: unknown;
|
||||
filterMatchesToLoad?: (match: DataRouteMatch) => boolean;
|
||||
skipLoaderErrorBubbling?: boolean;
|
||||
skipRevalidation?: boolean;
|
||||
dataStrategy?: DataStrategyFunction<unknown>;
|
||||
generateMiddlewareResponse?: (query: (r: Request, args?: {
|
||||
filterMatchesToLoad?: (match: DataRouteMatch) => boolean;
|
||||
}) => Promise<StaticHandlerContext | Response>) => MaybePromise<Response>;
|
||||
normalizePath?: (request: Request) => Path;
|
||||
}): Promise<StaticHandlerContext | Response>;
|
||||
/**
|
||||
* Perform a query for a specific route. Used for resource requests.
|
||||
*
|
||||
* @param request The request to query
|
||||
* @param opts Optional queryRoute options
|
||||
* @param opts.dataStrategy Alternate dataStrategy implementation
|
||||
* @param opts.generateMiddlewareResponse To enable middleware, provide a function
|
||||
* to generate a response to bubble back up the middleware chain
|
||||
* @param opts.requestContext Context object to pass to loaders/actions
|
||||
* @param opts.routeId The ID of the route to query
|
||||
* @param opts.normalizePath Normalize the request path
|
||||
|
||||
*/
|
||||
queryRoute(request: Request, opts?: {
|
||||
routeId?: string;
|
||||
requestContext?: unknown;
|
||||
dataStrategy?: DataStrategyFunction<unknown>;
|
||||
generateMiddlewareResponse?: (queryRoute: (r: Request) => Promise<Response>) => MaybePromise<Response>;
|
||||
normalizePath?: (request: Request) => Path;
|
||||
}): Promise<any>;
|
||||
}
|
||||
type ViewTransitionOpts = {
|
||||
currentLocation: Location;
|
||||
nextLocation: Location;
|
||||
};
|
||||
/**
|
||||
* Subscriber function signature for changes to router state
|
||||
*/
|
||||
interface RouterSubscriber {
|
||||
(state: RouterState, opts: {
|
||||
deletedFetchers: string[];
|
||||
newErrors: RouteData | null;
|
||||
viewTransitionOpts?: ViewTransitionOpts;
|
||||
flushSync: boolean;
|
||||
}): void;
|
||||
}
|
||||
/**
|
||||
* Function signature for determining the key to be used in scroll restoration
|
||||
* for a given location
|
||||
*/
|
||||
interface GetScrollRestorationKeyFunction {
|
||||
(location: Location, matches: UIMatch[]): string | null;
|
||||
}
|
||||
/**
|
||||
* Function signature for determining the current scroll position
|
||||
*/
|
||||
interface GetScrollPositionFunction {
|
||||
(): number;
|
||||
}
|
||||
/**
|
||||
* - "route": relative to the route hierarchy so `..` means remove all segments
|
||||
* of the current route even if it has many. For example, a `route("posts/:id")`
|
||||
* would have both `:id` and `posts` removed from the url.
|
||||
* - "path": relative to the pathname so `..` means remove one segment of the
|
||||
* pathname. For example, a `route("posts/:id")` would have only `:id` removed
|
||||
* from the url.
|
||||
*/
|
||||
type RelativeRoutingType = "route" | "path";
|
||||
type BaseNavigateOrFetchOptions = {
|
||||
preventScrollReset?: boolean;
|
||||
relative?: RelativeRoutingType;
|
||||
flushSync?: boolean;
|
||||
defaultShouldRevalidate?: boolean;
|
||||
};
|
||||
type BaseNavigateOptions = BaseNavigateOrFetchOptions & {
|
||||
replace?: boolean;
|
||||
state?: any;
|
||||
fromRouteId?: string;
|
||||
viewTransition?: boolean;
|
||||
mask?: To;
|
||||
};
|
||||
type BaseSubmissionOptions = {
|
||||
formMethod?: HTMLFormMethod;
|
||||
formEncType?: FormEncType;
|
||||
} & ({
|
||||
formData: FormData;
|
||||
body?: undefined;
|
||||
} | {
|
||||
formData?: undefined;
|
||||
body: any;
|
||||
});
|
||||
/**
|
||||
* Options for a navigate() call for a normal (non-submission) navigation
|
||||
*/
|
||||
type LinkNavigateOptions = BaseNavigateOptions;
|
||||
/**
|
||||
* Options for a navigate() call for a submission navigation
|
||||
*/
|
||||
type SubmissionNavigateOptions = BaseNavigateOptions & BaseSubmissionOptions;
|
||||
/**
|
||||
* Options to pass to navigate() for a navigation
|
||||
*/
|
||||
type RouterNavigateOptions = LinkNavigateOptions | SubmissionNavigateOptions;
|
||||
/**
|
||||
* Options for a fetch() load
|
||||
*/
|
||||
type LoadFetchOptions = BaseNavigateOrFetchOptions;
|
||||
/**
|
||||
* Options for a fetch() submission
|
||||
*/
|
||||
type SubmitFetchOptions = BaseNavigateOrFetchOptions & BaseSubmissionOptions;
|
||||
/**
|
||||
* Options to pass to fetch()
|
||||
*/
|
||||
type RouterFetchOptions = LoadFetchOptions | SubmitFetchOptions;
|
||||
/**
|
||||
* Potential states for state.navigation
|
||||
*/
|
||||
type NavigationStates = {
|
||||
Idle: {
|
||||
state: "idle";
|
||||
location: undefined;
|
||||
matches: undefined;
|
||||
historyAction: undefined;
|
||||
formMethod: undefined;
|
||||
formAction: undefined;
|
||||
formEncType: undefined;
|
||||
formData: undefined;
|
||||
json: undefined;
|
||||
text: undefined;
|
||||
};
|
||||
Loading: {
|
||||
state: "loading";
|
||||
location: Location;
|
||||
matches: DataRouteMatch[];
|
||||
historyAction: Action;
|
||||
formMethod: Submission["formMethod"] | undefined;
|
||||
formAction: Submission["formAction"] | undefined;
|
||||
formEncType: Submission["formEncType"] | undefined;
|
||||
formData: Submission["formData"] | undefined;
|
||||
json: Submission["json"] | undefined;
|
||||
text: Submission["text"] | undefined;
|
||||
};
|
||||
Submitting: {
|
||||
state: "submitting";
|
||||
location: Location;
|
||||
matches: DataRouteMatch[];
|
||||
historyAction: Action;
|
||||
formMethod: Submission["formMethod"];
|
||||
formAction: Submission["formAction"];
|
||||
formEncType: Submission["formEncType"];
|
||||
formData: Submission["formData"];
|
||||
json: Submission["json"];
|
||||
text: Submission["text"];
|
||||
};
|
||||
};
|
||||
type Navigation = NavigationStates[keyof NavigationStates];
|
||||
type RevalidationState = "idle" | "loading";
|
||||
/**
|
||||
* Potential states for fetchers
|
||||
*/
|
||||
type FetcherStates<TData = any> = {
|
||||
/**
|
||||
* The fetcher is not calling a loader or action
|
||||
*
|
||||
* ```tsx
|
||||
* fetcher.state === "idle"
|
||||
* ```
|
||||
*/
|
||||
Idle: {
|
||||
state: "idle";
|
||||
formMethod: undefined;
|
||||
formAction: undefined;
|
||||
formEncType: undefined;
|
||||
text: undefined;
|
||||
formData: undefined;
|
||||
json: undefined;
|
||||
/**
|
||||
* If the fetcher has never been called, this will be undefined.
|
||||
*/
|
||||
data: TData | undefined;
|
||||
};
|
||||
/**
|
||||
* The fetcher is loading data from a {@link LoaderFunction | loader} from a
|
||||
* call to {@link FetcherWithComponents.load | `fetcher.load`}.
|
||||
*
|
||||
* ```tsx
|
||||
* // somewhere
|
||||
* <button onClick={() => fetcher.load("/some/route") }>Load</button>
|
||||
*
|
||||
* // the state will update
|
||||
* fetcher.state === "loading"
|
||||
* ```
|
||||
*/
|
||||
Loading: {
|
||||
state: "loading";
|
||||
formMethod: Submission["formMethod"] | undefined;
|
||||
formAction: Submission["formAction"] | undefined;
|
||||
formEncType: Submission["formEncType"] | undefined;
|
||||
text: Submission["text"] | undefined;
|
||||
formData: Submission["formData"] | undefined;
|
||||
json: Submission["json"] | undefined;
|
||||
data: TData | undefined;
|
||||
};
|
||||
/**
|
||||
The fetcher is submitting to a {@link LoaderFunction} (GET) or {@link ActionFunction} (POST) from a {@link FetcherWithComponents.Form | `fetcher.Form`} or {@link FetcherWithComponents.submit | `fetcher.submit`}.
|
||||
|
||||
```tsx
|
||||
// somewhere
|
||||
<input
|
||||
onChange={e => {
|
||||
fetcher.submit(event.currentTarget.form, { method: "post" });
|
||||
}}
|
||||
/>
|
||||
|
||||
// the state will update
|
||||
fetcher.state === "submitting"
|
||||
|
||||
// and formData will be available
|
||||
fetcher.formData
|
||||
```
|
||||
*/
|
||||
Submitting: {
|
||||
state: "submitting";
|
||||
formMethod: Submission["formMethod"];
|
||||
formAction: Submission["formAction"];
|
||||
formEncType: Submission["formEncType"];
|
||||
text: Submission["text"];
|
||||
formData: Submission["formData"];
|
||||
json: Submission["json"];
|
||||
data: TData | undefined;
|
||||
};
|
||||
};
|
||||
type Fetcher<TData = any> = FetcherStates<TData>[keyof FetcherStates<TData>];
|
||||
interface BlockerBlocked {
|
||||
state: "blocked";
|
||||
reset: () => void;
|
||||
proceed: () => void;
|
||||
location: Location;
|
||||
}
|
||||
interface BlockerUnblocked {
|
||||
state: "unblocked";
|
||||
reset: undefined;
|
||||
proceed: undefined;
|
||||
location: undefined;
|
||||
}
|
||||
interface BlockerProceeding {
|
||||
state: "proceeding";
|
||||
reset: undefined;
|
||||
proceed: undefined;
|
||||
location: Location;
|
||||
}
|
||||
type Blocker = BlockerUnblocked | BlockerBlocked | BlockerProceeding;
|
||||
type BlockerFunction = (args: {
|
||||
currentLocation: Location;
|
||||
nextLocation: Location;
|
||||
historyAction: Action;
|
||||
}) => boolean;
|
||||
declare const IDLE_NAVIGATION: NavigationStates["Idle"];
|
||||
declare const IDLE_FETCHER: FetcherStates["Idle"];
|
||||
declare const IDLE_BLOCKER: BlockerUnblocked;
|
||||
/**
|
||||
* Create a router and listen to history POP navigations
|
||||
*/
|
||||
declare function createRouter(init: RouterInit): Router;
|
||||
interface CreateStaticHandlerOptions {
|
||||
basename?: string;
|
||||
mapRouteProperties?: MapRoutePropertiesFunction;
|
||||
instrumentations?: Pick<ServerInstrumentation, "route">[];
|
||||
future?: Partial<FutureConfig>;
|
||||
}
|
||||
|
||||
type ServerInstrumentation = {
|
||||
handler?: InstrumentRequestHandlerFunction;
|
||||
route?: InstrumentRouteFunction;
|
||||
};
|
||||
type ClientInstrumentation = {
|
||||
router?: InstrumentRouterFunction;
|
||||
route?: InstrumentRouteFunction;
|
||||
};
|
||||
type InstrumentRequestHandlerFunction = (handler: InstrumentableRequestHandler) => void;
|
||||
type InstrumentRouterFunction = (router: InstrumentableRouter) => void;
|
||||
type InstrumentRouteFunction = (route: InstrumentableRoute) => void;
|
||||
type InstrumentationHandlerResult = {
|
||||
status: "success";
|
||||
error: undefined;
|
||||
} | {
|
||||
status: "error";
|
||||
error: Error;
|
||||
};
|
||||
type InstrumentFunction<T> = (handler: () => Promise<InstrumentationHandlerResult>, info: T) => Promise<void>;
|
||||
type ReadonlyRequest = {
|
||||
method: string;
|
||||
url: string;
|
||||
headers: Pick<Headers, "get">;
|
||||
};
|
||||
type ReadonlyContext = MiddlewareEnabled extends true ? Pick<RouterContextProvider, "get"> : Readonly<AppLoadContext>;
|
||||
type InstrumentableRoute = {
|
||||
id: string;
|
||||
index: boolean | undefined;
|
||||
path: string | undefined;
|
||||
instrument(instrumentations: RouteInstrumentations): void;
|
||||
};
|
||||
type RouteInstrumentations = {
|
||||
lazy?: InstrumentFunction<RouteLazyInstrumentationInfo>;
|
||||
"lazy.loader"?: InstrumentFunction<RouteLazyInstrumentationInfo>;
|
||||
"lazy.action"?: InstrumentFunction<RouteLazyInstrumentationInfo>;
|
||||
"lazy.middleware"?: InstrumentFunction<RouteLazyInstrumentationInfo>;
|
||||
middleware?: InstrumentFunction<RouteHandlerInstrumentationInfo>;
|
||||
loader?: InstrumentFunction<RouteHandlerInstrumentationInfo>;
|
||||
action?: InstrumentFunction<RouteHandlerInstrumentationInfo>;
|
||||
};
|
||||
type RouteLazyInstrumentationInfo = undefined;
|
||||
type RouteHandlerInstrumentationInfo = Readonly<{
|
||||
request: ReadonlyRequest;
|
||||
params: LoaderFunctionArgs["params"];
|
||||
pattern: string;
|
||||
context: ReadonlyContext;
|
||||
}>;
|
||||
type InstrumentableRouter = {
|
||||
instrument(instrumentations: RouterInstrumentations): void;
|
||||
};
|
||||
type RouterInstrumentations = {
|
||||
navigate?: InstrumentFunction<RouterNavigationInstrumentationInfo>;
|
||||
fetch?: InstrumentFunction<RouterFetchInstrumentationInfo>;
|
||||
};
|
||||
type RouterNavigationInstrumentationInfo = Readonly<{
|
||||
to: string | number;
|
||||
currentUrl: string;
|
||||
formMethod?: HTMLFormMethod;
|
||||
formEncType?: FormEncType;
|
||||
formData?: FormData;
|
||||
body?: any;
|
||||
}>;
|
||||
type RouterFetchInstrumentationInfo = Readonly<{
|
||||
href: string;
|
||||
currentUrl: string;
|
||||
fetcherKey: string;
|
||||
formMethod?: HTMLFormMethod;
|
||||
formEncType?: FormEncType;
|
||||
formData?: FormData;
|
||||
body?: any;
|
||||
}>;
|
||||
type InstrumentableRequestHandler = {
|
||||
instrument(instrumentations: RequestHandlerInstrumentations): void;
|
||||
};
|
||||
type RequestHandlerInstrumentations = {
|
||||
request?: InstrumentFunction<RequestHandlerInstrumentationInfo>;
|
||||
};
|
||||
type RequestHandlerInstrumentationInfo = Readonly<{
|
||||
request: ReadonlyRequest;
|
||||
context: ReadonlyContext | undefined;
|
||||
}>;
|
||||
|
||||
export { type BlockerFunction as B, type ClientInstrumentation as C, type Fetcher as F, type GetScrollPositionFunction as G, type HydrationState as H, type InstrumentRequestHandlerFunction as I, type NavigationStates as N, type RouterInit as R, type StaticHandler as S, type Router as a, type Blocker as b, type RelativeRoutingType as c, type GetScrollRestorationKeyFunction as d, type StaticHandlerContext as e, type Navigation as f, type RouterState as g, type RouterSubscriber as h, type RouterNavigateOptions as i, type RouterFetchOptions as j, type RevalidationState as k, type ServerInstrumentation as l, type InstrumentRouterFunction as m, type InstrumentRouteFunction as n, type InstrumentationHandlerResult as o, IDLE_NAVIGATION as p, IDLE_FETCHER as q, IDLE_BLOCKER as r, createRouter as s, type FutureConfig as t, type CreateStaticHandlerOptions as u };
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
import { R as RouteModule, e as LinkDescriptor, L as Location, F as Func, f as Pretty, g as MetaDescriptor, G as GetLoaderData, h as ServerDataFunctionArgs, i as MiddlewareNextFunction, j as ClientDataFunctionArgs, D as DataStrategyResult, k as ServerDataFrom, N as Normalize, l as GetActionData } from '../../data-DEjBmEfD.mjs';
|
||||
import { R as RouteFiles, P as Pages } from '../../register-CmkRspdl.mjs';
|
||||
import 'react';
|
||||
|
||||
type MaybePromise<T> = T | Promise<T>;
|
||||
type Props = {
|
||||
params: unknown;
|
||||
loaderData: unknown;
|
||||
actionData: unknown;
|
||||
};
|
||||
type RouteInfo = Props & {
|
||||
module: RouteModule;
|
||||
matches: Array<MatchInfo>;
|
||||
};
|
||||
type MatchInfo = {
|
||||
id: string;
|
||||
module: RouteModule;
|
||||
};
|
||||
type MetaMatch<T extends MatchInfo> = Pretty<{
|
||||
id: T["id"];
|
||||
params: Record<string, string | undefined>;
|
||||
pathname: string;
|
||||
meta: MetaDescriptor[];
|
||||
/** @deprecated Use `MetaMatch.loaderData` instead */
|
||||
data: GetLoaderData<T["module"]>;
|
||||
loaderData: GetLoaderData<T["module"]>;
|
||||
handle?: unknown;
|
||||
error?: unknown;
|
||||
}>;
|
||||
type MetaMatches<T extends Array<MatchInfo>> = T extends [infer F extends MatchInfo, ...infer R extends Array<MatchInfo>] ? [MetaMatch<F>, ...MetaMatches<R>] : Array<MetaMatch<MatchInfo> | undefined>;
|
||||
type HasErrorBoundary<T extends RouteInfo> = T["module"] extends {
|
||||
ErrorBoundary: Func;
|
||||
} ? true : false;
|
||||
type CreateMetaArgs<T extends RouteInfo> = {
|
||||
/** This is the current router `Location` object. This is useful for generating tags for routes at specific paths or query parameters. */
|
||||
location: Location;
|
||||
/** {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route. */
|
||||
params: T["params"];
|
||||
/**
|
||||
* The return value for this route's server loader function
|
||||
*
|
||||
* @deprecated Use `Route.MetaArgs.loaderData` instead
|
||||
*/
|
||||
data: T["loaderData"] | (HasErrorBoundary<T> extends true ? undefined : never);
|
||||
/** The return value for this route's server loader function */
|
||||
loaderData: T["loaderData"] | (HasErrorBoundary<T> extends true ? undefined : never);
|
||||
/** Thrown errors that trigger error boundaries will be passed to the meta function. This is useful for generating metadata for error pages. */
|
||||
error?: unknown;
|
||||
/** An array of the current {@link https://api.reactrouter.com/v7/interfaces/react-router.UIMatch.html route matches}, including parent route matches. */
|
||||
matches: MetaMatches<T["matches"]>;
|
||||
};
|
||||
type MetaDescriptors = MetaDescriptor[];
|
||||
type HeadersArgs = {
|
||||
loaderHeaders: Headers;
|
||||
parentHeaders: Headers;
|
||||
actionHeaders: Headers;
|
||||
errorHeaders: Headers | undefined;
|
||||
};
|
||||
type CreateServerMiddlewareFunction<T extends RouteInfo> = (args: ServerDataFunctionArgs<T["params"]>, next: MiddlewareNextFunction<Response>) => MaybePromise<Response | void>;
|
||||
type CreateClientMiddlewareFunction<T extends RouteInfo> = (args: ClientDataFunctionArgs<T["params"]>, next: MiddlewareNextFunction<Record<string, DataStrategyResult>>) => MaybePromise<Record<string, DataStrategyResult> | void>;
|
||||
type CreateServerLoaderArgs<T extends RouteInfo> = ServerDataFunctionArgs<T["params"]>;
|
||||
type CreateClientLoaderArgs<T extends RouteInfo> = ClientDataFunctionArgs<T["params"]> & {
|
||||
/** This is an asynchronous function to get the data from the server loader for this route. On client-side navigations, this will make a {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API fetch} call to the React Router server loader. If you opt-into running your clientLoader on hydration, then this function will return the data that was already loaded on the server (via Promise.resolve). */
|
||||
serverLoader: () => Promise<ServerDataFrom<T["module"]["loader"]>>;
|
||||
};
|
||||
type CreateServerActionArgs<T extends RouteInfo> = ServerDataFunctionArgs<T["params"]>;
|
||||
type CreateClientActionArgs<T extends RouteInfo> = ClientDataFunctionArgs<T["params"]> & {
|
||||
/** This is an asynchronous function that makes the {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API fetch} call to the React Router server action for this route. */
|
||||
serverAction: () => Promise<ServerDataFrom<T["module"]["action"]>>;
|
||||
};
|
||||
type CreateHydrateFallbackProps<T extends RouteInfo, RSCEnabled extends boolean> = {
|
||||
params: T["params"];
|
||||
} & (RSCEnabled extends true ? {
|
||||
/** The data returned from the `loader` */
|
||||
loaderData?: ServerDataFrom<T["module"]["loader"]>;
|
||||
/** The data returned from the `action` following an action submission. */
|
||||
actionData?: ServerDataFrom<T["module"]["action"]>;
|
||||
} : {
|
||||
/** The data returned from the `loader` or `clientLoader` */
|
||||
loaderData?: T["loaderData"];
|
||||
/** The data returned from the `action` or `clientAction` following an action submission. */
|
||||
actionData?: T["actionData"];
|
||||
});
|
||||
type Match<T extends MatchInfo> = Pretty<{
|
||||
id: T["id"];
|
||||
params: Record<string, string | undefined>;
|
||||
pathname: string;
|
||||
/** @deprecated Use `Match.loaderData` instead */
|
||||
data: GetLoaderData<T["module"]>;
|
||||
loaderData: GetLoaderData<T["module"]>;
|
||||
handle: unknown;
|
||||
}>;
|
||||
type Matches<T extends Array<MatchInfo>> = T extends [infer F extends MatchInfo, ...infer R extends Array<MatchInfo>] ? [Match<F>, ...Matches<R>] : Array<Match<MatchInfo> | undefined>;
|
||||
type CreateComponentProps<T extends RouteInfo, RSCEnabled extends boolean> = {
|
||||
/**
|
||||
* {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route.
|
||||
* @example
|
||||
* // app/routes.ts
|
||||
* route("teams/:teamId", "./team.tsx"),
|
||||
*
|
||||
* // app/team.tsx
|
||||
* export default function Component({
|
||||
* params,
|
||||
* }: Route.ComponentProps) {
|
||||
* params.teamId;
|
||||
* // ^ string
|
||||
* }
|
||||
**/
|
||||
params: T["params"];
|
||||
/** An array of the current {@link https://api.reactrouter.com/v7/interfaces/react-router.UIMatch.html route matches}, including parent route matches. */
|
||||
matches: Matches<T["matches"]>;
|
||||
} & (RSCEnabled extends true ? {
|
||||
/** The data returned from the `loader` */
|
||||
loaderData: ServerDataFrom<T["module"]["loader"]>;
|
||||
/** The data returned from the `action` following an action submission. */
|
||||
actionData?: ServerDataFrom<T["module"]["action"]>;
|
||||
} : {
|
||||
/** The data returned from the `loader` or `clientLoader` */
|
||||
loaderData: T["loaderData"];
|
||||
/** The data returned from the `action` or `clientAction` following an action submission. */
|
||||
actionData?: T["actionData"];
|
||||
});
|
||||
type CreateErrorBoundaryProps<T extends RouteInfo, RSCEnabled extends boolean> = {
|
||||
/**
|
||||
* {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route.
|
||||
* @example
|
||||
* // app/routes.ts
|
||||
* route("teams/:teamId", "./team.tsx"),
|
||||
*
|
||||
* // app/team.tsx
|
||||
* export function ErrorBoundary({
|
||||
* params,
|
||||
* }: Route.ErrorBoundaryProps) {
|
||||
* params.teamId;
|
||||
* // ^ string
|
||||
* }
|
||||
**/
|
||||
params: T["params"];
|
||||
error: unknown;
|
||||
} & (RSCEnabled extends true ? {
|
||||
/** The data returned from the `loader` */
|
||||
loaderData?: ServerDataFrom<T["module"]["loader"]>;
|
||||
/** The data returned from the `action` following an action submission. */
|
||||
actionData?: ServerDataFrom<T["module"]["action"]>;
|
||||
} : {
|
||||
/** The data returned from the `loader` or `clientLoader` */
|
||||
loaderData?: T["loaderData"];
|
||||
/** The data returned from the `action` or `clientAction` following an action submission. */
|
||||
actionData?: T["actionData"];
|
||||
});
|
||||
type GetAnnotations<Info extends RouteInfo> = {
|
||||
LinkDescriptors: LinkDescriptor[];
|
||||
LinksFunction: () => LinkDescriptor[];
|
||||
MetaArgs: CreateMetaArgs<Info>;
|
||||
MetaDescriptors: MetaDescriptors;
|
||||
MetaFunction: (args: CreateMetaArgs<Info>) => MetaDescriptors;
|
||||
HeadersArgs: HeadersArgs;
|
||||
HeadersFunction: (args: HeadersArgs) => Headers | HeadersInit;
|
||||
MiddlewareFunction: CreateServerMiddlewareFunction<Info>;
|
||||
ClientMiddlewareFunction: CreateClientMiddlewareFunction<Info>;
|
||||
LoaderArgs: CreateServerLoaderArgs<Info>;
|
||||
ClientLoaderArgs: CreateClientLoaderArgs<Info>;
|
||||
ActionArgs: CreateServerActionArgs<Info>;
|
||||
ClientActionArgs: CreateClientActionArgs<Info>;
|
||||
HydrateFallbackProps: CreateHydrateFallbackProps<Info, false>;
|
||||
ServerHydrateFallbackProps: CreateHydrateFallbackProps<Info, true>;
|
||||
ComponentProps: CreateComponentProps<Info, false>;
|
||||
ServerComponentProps: CreateComponentProps<Info, true>;
|
||||
ErrorBoundaryProps: CreateErrorBoundaryProps<Info, false>;
|
||||
ServerErrorBoundaryProps: CreateErrorBoundaryProps<Info, true>;
|
||||
};
|
||||
|
||||
type Params<RouteFile extends keyof RouteFiles> = Normalize<Pages[RouteFiles[RouteFile]["page"]]["params"]>;
|
||||
|
||||
type GetInfo<T extends {
|
||||
file: keyof RouteFiles;
|
||||
module: RouteModule;
|
||||
}> = {
|
||||
params: Params<T["file"]>;
|
||||
loaderData: GetLoaderData<T["module"]>;
|
||||
actionData: GetActionData<T["module"]>;
|
||||
};
|
||||
|
||||
export type { GetAnnotations, GetInfo };
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
import { R as RouteModule, v as LinkDescriptor, L as Location, w as Func, x as Pretty, y as MetaDescriptor, G as GetLoaderData, z as ServerDataFunctionArgs, B as MiddlewareNextFunction, E as ClientDataFunctionArgs, I as DataStrategyResult, J as ServerDataFrom, N as Normalize, K as GetActionData } from '../../data-CjO11-hU.js';
|
||||
import { R as RouteFiles, P as Pages } from '../../register-roq_0qYo.js';
|
||||
import 'react';
|
||||
|
||||
type MaybePromise<T> = T | Promise<T>;
|
||||
type Props = {
|
||||
params: unknown;
|
||||
loaderData: unknown;
|
||||
actionData: unknown;
|
||||
};
|
||||
type RouteInfo = Props & {
|
||||
module: RouteModule;
|
||||
matches: Array<MatchInfo>;
|
||||
};
|
||||
type MatchInfo = {
|
||||
id: string;
|
||||
module: RouteModule;
|
||||
};
|
||||
type MetaMatch<T extends MatchInfo> = Pretty<{
|
||||
id: T["id"];
|
||||
params: Record<string, string | undefined>;
|
||||
pathname: string;
|
||||
meta: MetaDescriptor[];
|
||||
/** @deprecated Use `MetaMatch.loaderData` instead */
|
||||
data: GetLoaderData<T["module"]>;
|
||||
loaderData: GetLoaderData<T["module"]>;
|
||||
handle?: unknown;
|
||||
error?: unknown;
|
||||
}>;
|
||||
type MetaMatches<T extends Array<MatchInfo>> = T extends [infer F extends MatchInfo, ...infer R extends Array<MatchInfo>] ? [MetaMatch<F>, ...MetaMatches<R>] : Array<MetaMatch<MatchInfo> | undefined>;
|
||||
type HasErrorBoundary<T extends RouteInfo> = T["module"] extends {
|
||||
ErrorBoundary: Func;
|
||||
} ? true : false;
|
||||
type CreateMetaArgs<T extends RouteInfo> = {
|
||||
/** This is the current router `Location` object. This is useful for generating tags for routes at specific paths or query parameters. */
|
||||
location: Location;
|
||||
/** {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route. */
|
||||
params: T["params"];
|
||||
/**
|
||||
* The return value for this route's server loader function
|
||||
*
|
||||
* @deprecated Use `Route.MetaArgs.loaderData` instead
|
||||
*/
|
||||
data: T["loaderData"] | (HasErrorBoundary<T> extends true ? undefined : never);
|
||||
/** The return value for this route's server loader function */
|
||||
loaderData: T["loaderData"] | (HasErrorBoundary<T> extends true ? undefined : never);
|
||||
/** Thrown errors that trigger error boundaries will be passed to the meta function. This is useful for generating metadata for error pages. */
|
||||
error?: unknown;
|
||||
/** An array of the current {@link https://api.reactrouter.com/v7/interfaces/react-router.UIMatch.html route matches}, including parent route matches. */
|
||||
matches: MetaMatches<T["matches"]>;
|
||||
};
|
||||
type MetaDescriptors = MetaDescriptor[];
|
||||
type HeadersArgs = {
|
||||
loaderHeaders: Headers;
|
||||
parentHeaders: Headers;
|
||||
actionHeaders: Headers;
|
||||
errorHeaders: Headers | undefined;
|
||||
};
|
||||
type CreateServerMiddlewareFunction<T extends RouteInfo> = (args: ServerDataFunctionArgs<T["params"]>, next: MiddlewareNextFunction<Response>) => MaybePromise<Response | void>;
|
||||
type CreateClientMiddlewareFunction<T extends RouteInfo> = (args: ClientDataFunctionArgs<T["params"]>, next: MiddlewareNextFunction<Record<string, DataStrategyResult>>) => MaybePromise<Record<string, DataStrategyResult> | void>;
|
||||
type CreateServerLoaderArgs<T extends RouteInfo> = ServerDataFunctionArgs<T["params"]>;
|
||||
type CreateClientLoaderArgs<T extends RouteInfo> = ClientDataFunctionArgs<T["params"]> & {
|
||||
/** This is an asynchronous function to get the data from the server loader for this route. On client-side navigations, this will make a {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API fetch} call to the React Router server loader. If you opt-into running your clientLoader on hydration, then this function will return the data that was already loaded on the server (via Promise.resolve). */
|
||||
serverLoader: () => Promise<ServerDataFrom<T["module"]["loader"]>>;
|
||||
};
|
||||
type CreateServerActionArgs<T extends RouteInfo> = ServerDataFunctionArgs<T["params"]>;
|
||||
type CreateClientActionArgs<T extends RouteInfo> = ClientDataFunctionArgs<T["params"]> & {
|
||||
/** This is an asynchronous function that makes the {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API fetch} call to the React Router server action for this route. */
|
||||
serverAction: () => Promise<ServerDataFrom<T["module"]["action"]>>;
|
||||
};
|
||||
type CreateHydrateFallbackProps<T extends RouteInfo, RSCEnabled extends boolean> = {
|
||||
params: T["params"];
|
||||
} & (RSCEnabled extends true ? {
|
||||
/** The data returned from the `loader` */
|
||||
loaderData?: ServerDataFrom<T["module"]["loader"]>;
|
||||
/** The data returned from the `action` following an action submission. */
|
||||
actionData?: ServerDataFrom<T["module"]["action"]>;
|
||||
} : {
|
||||
/** The data returned from the `loader` or `clientLoader` */
|
||||
loaderData?: T["loaderData"];
|
||||
/** The data returned from the `action` or `clientAction` following an action submission. */
|
||||
actionData?: T["actionData"];
|
||||
});
|
||||
type Match<T extends MatchInfo> = Pretty<{
|
||||
id: T["id"];
|
||||
params: Record<string, string | undefined>;
|
||||
pathname: string;
|
||||
/** @deprecated Use `Match.loaderData` instead */
|
||||
data: GetLoaderData<T["module"]>;
|
||||
loaderData: GetLoaderData<T["module"]>;
|
||||
handle: unknown;
|
||||
}>;
|
||||
type Matches<T extends Array<MatchInfo>> = T extends [infer F extends MatchInfo, ...infer R extends Array<MatchInfo>] ? [Match<F>, ...Matches<R>] : Array<Match<MatchInfo> | undefined>;
|
||||
type CreateComponentProps<T extends RouteInfo, RSCEnabled extends boolean> = {
|
||||
/**
|
||||
* {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route.
|
||||
* @example
|
||||
* // app/routes.ts
|
||||
* route("teams/:teamId", "./team.tsx"),
|
||||
*
|
||||
* // app/team.tsx
|
||||
* export default function Component({
|
||||
* params,
|
||||
* }: Route.ComponentProps) {
|
||||
* params.teamId;
|
||||
* // ^ string
|
||||
* }
|
||||
**/
|
||||
params: T["params"];
|
||||
/** An array of the current {@link https://api.reactrouter.com/v7/interfaces/react-router.UIMatch.html route matches}, including parent route matches. */
|
||||
matches: Matches<T["matches"]>;
|
||||
} & (RSCEnabled extends true ? {
|
||||
/** The data returned from the `loader` */
|
||||
loaderData: ServerDataFrom<T["module"]["loader"]>;
|
||||
/** The data returned from the `action` following an action submission. */
|
||||
actionData?: ServerDataFrom<T["module"]["action"]>;
|
||||
} : {
|
||||
/** The data returned from the `loader` or `clientLoader` */
|
||||
loaderData: T["loaderData"];
|
||||
/** The data returned from the `action` or `clientAction` following an action submission. */
|
||||
actionData?: T["actionData"];
|
||||
});
|
||||
type CreateErrorBoundaryProps<T extends RouteInfo, RSCEnabled extends boolean> = {
|
||||
/**
|
||||
* {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route.
|
||||
* @example
|
||||
* // app/routes.ts
|
||||
* route("teams/:teamId", "./team.tsx"),
|
||||
*
|
||||
* // app/team.tsx
|
||||
* export function ErrorBoundary({
|
||||
* params,
|
||||
* }: Route.ErrorBoundaryProps) {
|
||||
* params.teamId;
|
||||
* // ^ string
|
||||
* }
|
||||
**/
|
||||
params: T["params"];
|
||||
error: unknown;
|
||||
} & (RSCEnabled extends true ? {
|
||||
/** The data returned from the `loader` */
|
||||
loaderData?: ServerDataFrom<T["module"]["loader"]>;
|
||||
/** The data returned from the `action` following an action submission. */
|
||||
actionData?: ServerDataFrom<T["module"]["action"]>;
|
||||
} : {
|
||||
/** The data returned from the `loader` or `clientLoader` */
|
||||
loaderData?: T["loaderData"];
|
||||
/** The data returned from the `action` or `clientAction` following an action submission. */
|
||||
actionData?: T["actionData"];
|
||||
});
|
||||
type GetAnnotations<Info extends RouteInfo> = {
|
||||
LinkDescriptors: LinkDescriptor[];
|
||||
LinksFunction: () => LinkDescriptor[];
|
||||
MetaArgs: CreateMetaArgs<Info>;
|
||||
MetaDescriptors: MetaDescriptors;
|
||||
MetaFunction: (args: CreateMetaArgs<Info>) => MetaDescriptors;
|
||||
HeadersArgs: HeadersArgs;
|
||||
HeadersFunction: (args: HeadersArgs) => Headers | HeadersInit;
|
||||
MiddlewareFunction: CreateServerMiddlewareFunction<Info>;
|
||||
ClientMiddlewareFunction: CreateClientMiddlewareFunction<Info>;
|
||||
LoaderArgs: CreateServerLoaderArgs<Info>;
|
||||
ClientLoaderArgs: CreateClientLoaderArgs<Info>;
|
||||
ActionArgs: CreateServerActionArgs<Info>;
|
||||
ClientActionArgs: CreateClientActionArgs<Info>;
|
||||
HydrateFallbackProps: CreateHydrateFallbackProps<Info, false>;
|
||||
ServerHydrateFallbackProps: CreateHydrateFallbackProps<Info, true>;
|
||||
ComponentProps: CreateComponentProps<Info, false>;
|
||||
ServerComponentProps: CreateComponentProps<Info, true>;
|
||||
ErrorBoundaryProps: CreateErrorBoundaryProps<Info, false>;
|
||||
ServerErrorBoundaryProps: CreateErrorBoundaryProps<Info, true>;
|
||||
};
|
||||
|
||||
type Params<RouteFile extends keyof RouteFiles> = Normalize<Pages[RouteFiles[RouteFile]["page"]]["params"]>;
|
||||
|
||||
type GetInfo<T extends {
|
||||
file: keyof RouteFiles;
|
||||
module: RouteModule;
|
||||
}> = {
|
||||
params: Params<T["file"]>;
|
||||
loaderData: GetLoaderData<T["module"]>;
|
||||
actionData: GetActionData<T["module"]>;
|
||||
};
|
||||
|
||||
export type { GetAnnotations, GetInfo };
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
"use strict";/**
|
||||
* react-router v7.18.0
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* react-router v7.18.0
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { R as RouteModule } from './data-DEjBmEfD.mjs';
|
||||
|
||||
/**
|
||||
* Apps can use this interface to "register" app-wide types for React Router via interface declaration merging and module augmentation.
|
||||
* React Router should handle this for you via type generation.
|
||||
*
|
||||
* For more on declaration merging and module augmentation, see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation .
|
||||
*/
|
||||
interface Register {
|
||||
}
|
||||
type AnyParams = Record<string, string | undefined>;
|
||||
type AnyPages = Record<string, {
|
||||
params: AnyParams;
|
||||
}>;
|
||||
type Pages = Register extends {
|
||||
pages: infer Registered extends AnyPages;
|
||||
} ? Registered : AnyPages;
|
||||
type AnyRouteFiles = Record<string, {
|
||||
id: string;
|
||||
page: string;
|
||||
}>;
|
||||
type RouteFiles = Register extends {
|
||||
routeFiles: infer Registered extends AnyRouteFiles;
|
||||
} ? Registered : AnyRouteFiles;
|
||||
type AnyRouteModules = Record<string, RouteModule>;
|
||||
type RouteModules = Register extends {
|
||||
routeModules: infer Registered extends AnyRouteModules;
|
||||
} ? Registered : AnyRouteModules;
|
||||
|
||||
export type { Pages as P, RouteFiles as R, RouteModules as a, Register as b };
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { R as RouteModule } from './data-CjO11-hU.js';
|
||||
|
||||
/**
|
||||
* Apps can use this interface to "register" app-wide types for React Router via interface declaration merging and module augmentation.
|
||||
* React Router should handle this for you via type generation.
|
||||
*
|
||||
* For more on declaration merging and module augmentation, see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation .
|
||||
*/
|
||||
interface Register {
|
||||
}
|
||||
type AnyParams = Record<string, string | undefined>;
|
||||
type AnyPages = Record<string, {
|
||||
params: AnyParams;
|
||||
}>;
|
||||
type Pages = Register extends {
|
||||
pages: infer Registered extends AnyPages;
|
||||
} ? Registered : AnyPages;
|
||||
type AnyRouteFiles = Record<string, {
|
||||
id: string;
|
||||
page: string;
|
||||
}>;
|
||||
type RouteFiles = Register extends {
|
||||
routeFiles: infer Registered extends AnyRouteFiles;
|
||||
} ? Registered : AnyRouteFiles;
|
||||
type AnyRouteModules = Record<string, RouteModule>;
|
||||
type RouteModules = Register extends {
|
||||
routeModules: infer Registered extends AnyRouteModules;
|
||||
} ? Registered : AnyRouteModules;
|
||||
|
||||
export type { Pages as P, RouteFiles as R, RouteModules as a, Register as b };
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
import * as React from 'react';
|
||||
import { R as RouterInit } from './instrumentation-Dkmpzd13.js';
|
||||
import { L as Location, C as ClientActionFunction, a as ClientLoaderFunction, b as LinksFunction, M as MetaFunction, S as ShouldRevalidateFunction, P as Params, c as RouterContextProvider, A as ActionFunction, H as HeadersFunction, d as LoaderFunction } from './data-CjO11-hU.js';
|
||||
|
||||
declare function getRequest(): Request;
|
||||
type RSCRouteConfigEntryBase = {
|
||||
action?: ActionFunction;
|
||||
clientAction?: ClientActionFunction;
|
||||
clientLoader?: ClientLoaderFunction;
|
||||
ErrorBoundary?: React.ComponentType<any>;
|
||||
handle?: any;
|
||||
headers?: HeadersFunction;
|
||||
HydrateFallback?: React.ComponentType<any>;
|
||||
Layout?: React.ComponentType<any>;
|
||||
links?: LinksFunction;
|
||||
loader?: LoaderFunction;
|
||||
meta?: MetaFunction;
|
||||
shouldRevalidate?: ShouldRevalidateFunction;
|
||||
};
|
||||
type RSCRouteConfigEntry = RSCRouteConfigEntryBase & {
|
||||
id: string;
|
||||
path?: string;
|
||||
Component?: React.ComponentType<any>;
|
||||
lazy?: () => Promise<RSCRouteConfigEntryBase & ({
|
||||
default?: React.ComponentType<any>;
|
||||
Component?: never;
|
||||
} | {
|
||||
default?: never;
|
||||
Component?: React.ComponentType<any>;
|
||||
})>;
|
||||
} & ({
|
||||
index: true;
|
||||
} | {
|
||||
children?: RSCRouteConfigEntry[];
|
||||
});
|
||||
type RSCRouteConfig = Array<RSCRouteConfigEntry>;
|
||||
type RSCRouteManifest = {
|
||||
clientAction?: ClientActionFunction;
|
||||
clientLoader?: ClientLoaderFunction;
|
||||
element?: React.ReactElement | false;
|
||||
errorElement?: React.ReactElement;
|
||||
handle?: any;
|
||||
hasAction: boolean;
|
||||
hasComponent: boolean;
|
||||
hasErrorBoundary: boolean;
|
||||
hasLoader: boolean;
|
||||
hydrateFallbackElement?: React.ReactElement;
|
||||
id: string;
|
||||
index?: boolean;
|
||||
links?: LinksFunction;
|
||||
meta?: MetaFunction;
|
||||
parentId?: string;
|
||||
path?: string;
|
||||
shouldRevalidate?: ShouldRevalidateFunction;
|
||||
};
|
||||
type RSCRouteMatch = RSCRouteManifest & {
|
||||
params: Params;
|
||||
pathname: string;
|
||||
pathnameBase: string;
|
||||
};
|
||||
type RSCRenderPayload = {
|
||||
type: "render";
|
||||
actionData: Record<string, any> | null;
|
||||
basename: string | undefined;
|
||||
errors: Record<string, any> | null;
|
||||
loaderData: Record<string, any>;
|
||||
location: Location;
|
||||
routeDiscovery: RouteDiscovery;
|
||||
matches: RSCRouteMatch[];
|
||||
patches?: Promise<RSCRouteManifest[]>;
|
||||
nonce?: string;
|
||||
formState?: unknown;
|
||||
};
|
||||
type RSCManifestPayload = {
|
||||
type: "manifest";
|
||||
patches: Promise<RSCRouteManifest[]>;
|
||||
};
|
||||
type RSCActionPayload = {
|
||||
type: "action";
|
||||
actionResult: Promise<unknown>;
|
||||
rerender?: Promise<RSCRenderPayload | RSCRedirectPayload>;
|
||||
};
|
||||
type RSCRedirectPayload = {
|
||||
type: "redirect";
|
||||
status: number;
|
||||
location: string;
|
||||
replace: boolean;
|
||||
reload: boolean;
|
||||
actionResult?: Promise<unknown>;
|
||||
};
|
||||
type RSCPayload = RSCRenderPayload | RSCManifestPayload | RSCActionPayload | RSCRedirectPayload;
|
||||
type RSCMatch = {
|
||||
statusCode: number;
|
||||
headers: Headers;
|
||||
payload: RSCPayload;
|
||||
};
|
||||
type DecodeActionFunction = (formData: FormData) => Promise<() => Promise<unknown>>;
|
||||
type DecodeFormStateFunction = (result: unknown, formData: FormData) => unknown;
|
||||
type DecodeReplyFunction = (reply: FormData | string, options: {
|
||||
temporaryReferences: unknown;
|
||||
}) => Promise<unknown[]>;
|
||||
type LoadServerActionFunction = (id: string) => Promise<Function>;
|
||||
type RouteDiscovery = {
|
||||
mode: "lazy";
|
||||
manifestPath?: string | undefined;
|
||||
} | {
|
||||
mode: "initial";
|
||||
};
|
||||
/**
|
||||
* Matches the given routes to a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
|
||||
* and returns an [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
|
||||
* encoding an {@link unstable_RSCPayload} for consumption by an [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* enabled client router.
|
||||
*
|
||||
* @example
|
||||
* import {
|
||||
* createTemporaryReferenceSet,
|
||||
* decodeAction,
|
||||
* decodeReply,
|
||||
* loadServerAction,
|
||||
* renderToReadableStream,
|
||||
* } from "@vitejs/plugin-rsc/rsc";
|
||||
* import { unstable_matchRSCServerRequest as matchRSCServerRequest } from "react-router";
|
||||
*
|
||||
* matchRSCServerRequest({
|
||||
* createTemporaryReferenceSet,
|
||||
* decodeAction,
|
||||
* decodeFormState,
|
||||
* decodeReply,
|
||||
* loadServerAction,
|
||||
* request,
|
||||
* routes: routes(),
|
||||
* generateResponse(match) {
|
||||
* return new Response(
|
||||
* renderToReadableStream(match.payload),
|
||||
* {
|
||||
* status: match.statusCode,
|
||||
* headers: match.headers,
|
||||
* }
|
||||
* );
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* @name unstable_matchRSCServerRequest
|
||||
* @public
|
||||
* @category RSC
|
||||
* @mode data
|
||||
* @param opts Options
|
||||
* @param opts.allowedActionOrigins Origin patterns that are allowed to execute actions.
|
||||
* @param opts.basename The basename to use when matching the request.
|
||||
* @param opts.createTemporaryReferenceSet A function that returns a temporary
|
||||
* reference set for the request, used to track temporary references in the [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* stream.
|
||||
* @param opts.decodeAction Your `react-server-dom-xyz/server`'s `decodeAction`
|
||||
* function, responsible for loading a server action.
|
||||
* @param opts.decodeFormState A function responsible for decoding form state for
|
||||
* progressively enhanceable forms with React's [`useActionState`](https://react.dev/reference/react/useActionState)
|
||||
* using your `react-server-dom-xyz/server`'s `decodeFormState`.
|
||||
* @param opts.decodeReply Your `react-server-dom-xyz/server`'s `decodeReply`
|
||||
* function, used to decode the server function's arguments and bind them to the
|
||||
* implementation for invocation by the router.
|
||||
* @param opts.generateResponse A function responsible for using your
|
||||
* `renderToReadableStream` to generate a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
|
||||
* encoding the {@link unstable_RSCPayload}.
|
||||
* @param opts.loadServerAction Your `react-server-dom-xyz/server`'s
|
||||
* `loadServerAction` function, used to load a server action by ID.
|
||||
* @param opts.onError An optional error handler that will be called with any
|
||||
* errors that occur during the request processing.
|
||||
* @param opts.request The [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
|
||||
* to match against.
|
||||
* @param opts.requestContext An instance of {@link RouterContextProvider}
|
||||
* that should be created per request, to be passed to [`action`](../../start/data/route-object#action)s,
|
||||
* [`loader`](../../start/data/route-object#loader)s and [middleware](../../how-to/middleware).
|
||||
* @param opts.routeDiscovery The route discovery configuration, used to determine how the router should discover new routes during navigations.
|
||||
* @param opts.routes Your {@link unstable_RSCRouteConfigEntry | route definitions}.
|
||||
* @returns A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
|
||||
* that contains the [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* data for hydration.
|
||||
*/
|
||||
declare function matchRSCServerRequest({ allowedActionOrigins, createTemporaryReferenceSet, basename, decodeReply, requestContext, routeDiscovery, loadServerAction, decodeAction, decodeFormState, onError, request, routes, generateResponse, }: {
|
||||
allowedActionOrigins?: string[];
|
||||
createTemporaryReferenceSet: () => unknown;
|
||||
basename?: string;
|
||||
decodeReply?: DecodeReplyFunction;
|
||||
decodeAction?: DecodeActionFunction;
|
||||
decodeFormState?: DecodeFormStateFunction;
|
||||
requestContext?: RouterContextProvider;
|
||||
loadServerAction?: LoadServerActionFunction;
|
||||
onError?: (error: unknown) => void;
|
||||
request: Request;
|
||||
routes: RSCRouteConfigEntry[];
|
||||
routeDiscovery?: RouteDiscovery;
|
||||
generateResponse: (match: RSCMatch, { onError, temporaryReferences, }: {
|
||||
onError(error: unknown): string | undefined;
|
||||
temporaryReferences: unknown;
|
||||
}) => Response;
|
||||
}): Promise<Response>;
|
||||
|
||||
type BrowserCreateFromReadableStreamFunction = (body: ReadableStream<Uint8Array>, { temporaryReferences, }: {
|
||||
temporaryReferences: unknown;
|
||||
}) => Promise<unknown>;
|
||||
type EncodeReplyFunction = (args: unknown[], options: {
|
||||
temporaryReferences: unknown;
|
||||
}) => Promise<BodyInit>;
|
||||
/**
|
||||
* Create a React `callServer` implementation for React Router.
|
||||
*
|
||||
* @example
|
||||
* import {
|
||||
* createFromReadableStream,
|
||||
* createTemporaryReferenceSet,
|
||||
* encodeReply,
|
||||
* setServerCallback,
|
||||
* } from "@vitejs/plugin-rsc/browser";
|
||||
* import { unstable_createCallServer as createCallServer } from "react-router";
|
||||
*
|
||||
* setServerCallback(
|
||||
* createCallServer({
|
||||
* createFromReadableStream,
|
||||
* createTemporaryReferenceSet,
|
||||
* encodeReply,
|
||||
* })
|
||||
* );
|
||||
*
|
||||
* @name unstable_createCallServer
|
||||
* @public
|
||||
* @category RSC
|
||||
* @mode data
|
||||
* @param opts Options
|
||||
* @param opts.createFromReadableStream Your `react-server-dom-xyz/client`'s
|
||||
* `createFromReadableStream`. Used to decode payloads from the server.
|
||||
* @param opts.createTemporaryReferenceSet A function that creates a temporary
|
||||
* reference set for the [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* payload.
|
||||
* @param opts.encodeReply Your `react-server-dom-xyz/client`'s `encodeReply`.
|
||||
* Used when sending payloads to the server.
|
||||
* @param opts.fetch Optional [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
|
||||
* implementation. Defaults to global [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch).
|
||||
* @returns A function that can be used to call server actions.
|
||||
*/
|
||||
declare function createCallServer({ createFromReadableStream, createTemporaryReferenceSet, encodeReply, fetch: fetchImplementation, }: {
|
||||
createFromReadableStream: BrowserCreateFromReadableStreamFunction;
|
||||
createTemporaryReferenceSet: () => unknown;
|
||||
encodeReply: EncodeReplyFunction;
|
||||
fetch?: (request: Request) => Promise<Response>;
|
||||
}): (id: string, args: unknown[]) => Promise<unknown>;
|
||||
/**
|
||||
* Props for the {@link unstable_RSCHydratedRouter} component.
|
||||
*
|
||||
* @name unstable_RSCHydratedRouterProps
|
||||
* @category Types
|
||||
*/
|
||||
interface RSCHydratedRouterProps {
|
||||
/**
|
||||
* Your `react-server-dom-xyz/client`'s `createFromReadableStream` function,
|
||||
* used to decode payloads from the server.
|
||||
*/
|
||||
createFromReadableStream: BrowserCreateFromReadableStreamFunction;
|
||||
/**
|
||||
* Optional fetch implementation. Defaults to global [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch).
|
||||
*/
|
||||
fetch?: (request: Request) => Promise<Response>;
|
||||
/**
|
||||
* The decoded {@link unstable_RSCPayload} to hydrate.
|
||||
*/
|
||||
payload: RSCPayload;
|
||||
/**
|
||||
* A function that returns an {@link RouterContextProvider} instance
|
||||
* which is provided as the `context` argument to client [`action`](../../start/data/route-object#action)s,
|
||||
* [`loader`](../../start/data/route-object#loader)s and [middleware](../../how-to/middleware).
|
||||
* This function is called to generate a fresh `context` instance on each
|
||||
* navigation or fetcher call.
|
||||
*/
|
||||
getContext?: RouterInit["getContext"];
|
||||
}
|
||||
/**
|
||||
* Hydrates a server rendered {@link unstable_RSCPayload} in the browser.
|
||||
*
|
||||
* @example
|
||||
* import { startTransition, StrictMode } from "react";
|
||||
* import { hydrateRoot } from "react-dom/client";
|
||||
* import {
|
||||
* unstable_getRSCStream as getRSCStream,
|
||||
* unstable_RSCHydratedRouter as RSCHydratedRouter,
|
||||
* } from "react-router";
|
||||
* import type { unstable_RSCPayload as RSCPayload } from "react-router";
|
||||
*
|
||||
* createFromReadableStream(getRSCStream()).then((payload) =>
|
||||
* startTransition(async () => {
|
||||
* hydrateRoot(
|
||||
* document,
|
||||
* <StrictMode>
|
||||
* <RSCHydratedRouter
|
||||
* createFromReadableStream={createFromReadableStream}
|
||||
* payload={payload}
|
||||
* />
|
||||
* </StrictMode>,
|
||||
* { formState: await getFormState(payload) },
|
||||
* );
|
||||
* }),
|
||||
* );
|
||||
*
|
||||
* @name unstable_RSCHydratedRouter
|
||||
* @public
|
||||
* @category RSC
|
||||
* @mode data
|
||||
* @param props Props
|
||||
* @param {unstable_RSCHydratedRouterProps.createFromReadableStream} props.createFromReadableStream n/a
|
||||
* @param {unstable_RSCHydratedRouterProps.fetch} props.fetch n/a
|
||||
* @param {unstable_RSCHydratedRouterProps.getContext} props.getContext n/a
|
||||
* @param {unstable_RSCHydratedRouterProps.payload} props.payload n/a
|
||||
* @returns A hydrated {@link DataRouter} that can be used to navigate and
|
||||
* render routes.
|
||||
*/
|
||||
declare function RSCHydratedRouter({ createFromReadableStream, fetch: fetchImplementation, payload, getContext, }: RSCHydratedRouterProps): React.JSX.Element;
|
||||
|
||||
export { type BrowserCreateFromReadableStreamFunction as B, type DecodeActionFunction as D, type EncodeReplyFunction as E, type LoadServerActionFunction as L, RSCHydratedRouter as R, type DecodeFormStateFunction as a, type DecodeReplyFunction as b, createCallServer as c, type RSCManifestPayload as d, type RSCPayload as e, type RSCRenderPayload as f, getRequest as g, type RSCHydratedRouterProps as h, type RSCMatch as i, type RSCRouteManifest as j, type RSCRouteMatch as k, type RSCRouteConfigEntry as l, matchRSCServerRequest as m, type RSCRouteConfig as n };
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
import * as React from 'react';
|
||||
import { R as RouterInit } from './context-CeD5LmaF.mjs';
|
||||
import { L as Location, C as ClientActionFunction, a as ClientLoaderFunction, b as LinksFunction, M as MetaFunction, S as ShouldRevalidateFunction, P as Params, c as RouterContextProvider, A as ActionFunction, H as HeadersFunction, d as LoaderFunction } from './data-DEjBmEfD.mjs';
|
||||
|
||||
declare function getRequest(): Request;
|
||||
type RSCRouteConfigEntryBase = {
|
||||
action?: ActionFunction;
|
||||
clientAction?: ClientActionFunction;
|
||||
clientLoader?: ClientLoaderFunction;
|
||||
ErrorBoundary?: React.ComponentType<any>;
|
||||
handle?: any;
|
||||
headers?: HeadersFunction;
|
||||
HydrateFallback?: React.ComponentType<any>;
|
||||
Layout?: React.ComponentType<any>;
|
||||
links?: LinksFunction;
|
||||
loader?: LoaderFunction;
|
||||
meta?: MetaFunction;
|
||||
shouldRevalidate?: ShouldRevalidateFunction;
|
||||
};
|
||||
type RSCRouteConfigEntry = RSCRouteConfigEntryBase & {
|
||||
id: string;
|
||||
path?: string;
|
||||
Component?: React.ComponentType<any>;
|
||||
lazy?: () => Promise<RSCRouteConfigEntryBase & ({
|
||||
default?: React.ComponentType<any>;
|
||||
Component?: never;
|
||||
} | {
|
||||
default?: never;
|
||||
Component?: React.ComponentType<any>;
|
||||
})>;
|
||||
} & ({
|
||||
index: true;
|
||||
} | {
|
||||
children?: RSCRouteConfigEntry[];
|
||||
});
|
||||
type RSCRouteConfig = Array<RSCRouteConfigEntry>;
|
||||
type RSCRouteManifest = {
|
||||
clientAction?: ClientActionFunction;
|
||||
clientLoader?: ClientLoaderFunction;
|
||||
element?: React.ReactElement | false;
|
||||
errorElement?: React.ReactElement;
|
||||
handle?: any;
|
||||
hasAction: boolean;
|
||||
hasComponent: boolean;
|
||||
hasErrorBoundary: boolean;
|
||||
hasLoader: boolean;
|
||||
hydrateFallbackElement?: React.ReactElement;
|
||||
id: string;
|
||||
index?: boolean;
|
||||
links?: LinksFunction;
|
||||
meta?: MetaFunction;
|
||||
parentId?: string;
|
||||
path?: string;
|
||||
shouldRevalidate?: ShouldRevalidateFunction;
|
||||
};
|
||||
type RSCRouteMatch = RSCRouteManifest & {
|
||||
params: Params;
|
||||
pathname: string;
|
||||
pathnameBase: string;
|
||||
};
|
||||
type RSCRenderPayload = {
|
||||
type: "render";
|
||||
actionData: Record<string, any> | null;
|
||||
basename: string | undefined;
|
||||
errors: Record<string, any> | null;
|
||||
loaderData: Record<string, any>;
|
||||
location: Location;
|
||||
routeDiscovery: RouteDiscovery;
|
||||
matches: RSCRouteMatch[];
|
||||
patches?: Promise<RSCRouteManifest[]>;
|
||||
nonce?: string;
|
||||
formState?: unknown;
|
||||
};
|
||||
type RSCManifestPayload = {
|
||||
type: "manifest";
|
||||
patches: Promise<RSCRouteManifest[]>;
|
||||
};
|
||||
type RSCActionPayload = {
|
||||
type: "action";
|
||||
actionResult: Promise<unknown>;
|
||||
rerender?: Promise<RSCRenderPayload | RSCRedirectPayload>;
|
||||
};
|
||||
type RSCRedirectPayload = {
|
||||
type: "redirect";
|
||||
status: number;
|
||||
location: string;
|
||||
replace: boolean;
|
||||
reload: boolean;
|
||||
actionResult?: Promise<unknown>;
|
||||
};
|
||||
type RSCPayload = RSCRenderPayload | RSCManifestPayload | RSCActionPayload | RSCRedirectPayload;
|
||||
type RSCMatch = {
|
||||
statusCode: number;
|
||||
headers: Headers;
|
||||
payload: RSCPayload;
|
||||
};
|
||||
type DecodeActionFunction = (formData: FormData) => Promise<() => Promise<unknown>>;
|
||||
type DecodeFormStateFunction = (result: unknown, formData: FormData) => unknown;
|
||||
type DecodeReplyFunction = (reply: FormData | string, options: {
|
||||
temporaryReferences: unknown;
|
||||
}) => Promise<unknown[]>;
|
||||
type LoadServerActionFunction = (id: string) => Promise<Function>;
|
||||
type RouteDiscovery = {
|
||||
mode: "lazy";
|
||||
manifestPath?: string | undefined;
|
||||
} | {
|
||||
mode: "initial";
|
||||
};
|
||||
/**
|
||||
* Matches the given routes to a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
|
||||
* and returns an [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
|
||||
* encoding an {@link unstable_RSCPayload} for consumption by an [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* enabled client router.
|
||||
*
|
||||
* @example
|
||||
* import {
|
||||
* createTemporaryReferenceSet,
|
||||
* decodeAction,
|
||||
* decodeReply,
|
||||
* loadServerAction,
|
||||
* renderToReadableStream,
|
||||
* } from "@vitejs/plugin-rsc/rsc";
|
||||
* import { unstable_matchRSCServerRequest as matchRSCServerRequest } from "react-router";
|
||||
*
|
||||
* matchRSCServerRequest({
|
||||
* createTemporaryReferenceSet,
|
||||
* decodeAction,
|
||||
* decodeFormState,
|
||||
* decodeReply,
|
||||
* loadServerAction,
|
||||
* request,
|
||||
* routes: routes(),
|
||||
* generateResponse(match) {
|
||||
* return new Response(
|
||||
* renderToReadableStream(match.payload),
|
||||
* {
|
||||
* status: match.statusCode,
|
||||
* headers: match.headers,
|
||||
* }
|
||||
* );
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* @name unstable_matchRSCServerRequest
|
||||
* @public
|
||||
* @category RSC
|
||||
* @mode data
|
||||
* @param opts Options
|
||||
* @param opts.allowedActionOrigins Origin patterns that are allowed to execute actions.
|
||||
* @param opts.basename The basename to use when matching the request.
|
||||
* @param opts.createTemporaryReferenceSet A function that returns a temporary
|
||||
* reference set for the request, used to track temporary references in the [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* stream.
|
||||
* @param opts.decodeAction Your `react-server-dom-xyz/server`'s `decodeAction`
|
||||
* function, responsible for loading a server action.
|
||||
* @param opts.decodeFormState A function responsible for decoding form state for
|
||||
* progressively enhanceable forms with React's [`useActionState`](https://react.dev/reference/react/useActionState)
|
||||
* using your `react-server-dom-xyz/server`'s `decodeFormState`.
|
||||
* @param opts.decodeReply Your `react-server-dom-xyz/server`'s `decodeReply`
|
||||
* function, used to decode the server function's arguments and bind them to the
|
||||
* implementation for invocation by the router.
|
||||
* @param opts.generateResponse A function responsible for using your
|
||||
* `renderToReadableStream` to generate a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
|
||||
* encoding the {@link unstable_RSCPayload}.
|
||||
* @param opts.loadServerAction Your `react-server-dom-xyz/server`'s
|
||||
* `loadServerAction` function, used to load a server action by ID.
|
||||
* @param opts.onError An optional error handler that will be called with any
|
||||
* errors that occur during the request processing.
|
||||
* @param opts.request The [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
|
||||
* to match against.
|
||||
* @param opts.requestContext An instance of {@link RouterContextProvider}
|
||||
* that should be created per request, to be passed to [`action`](../../start/data/route-object#action)s,
|
||||
* [`loader`](../../start/data/route-object#loader)s and [middleware](../../how-to/middleware).
|
||||
* @param opts.routeDiscovery The route discovery configuration, used to determine how the router should discover new routes during navigations.
|
||||
* @param opts.routes Your {@link unstable_RSCRouteConfigEntry | route definitions}.
|
||||
* @returns A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
|
||||
* that contains the [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* data for hydration.
|
||||
*/
|
||||
declare function matchRSCServerRequest({ allowedActionOrigins, createTemporaryReferenceSet, basename, decodeReply, requestContext, routeDiscovery, loadServerAction, decodeAction, decodeFormState, onError, request, routes, generateResponse, }: {
|
||||
allowedActionOrigins?: string[];
|
||||
createTemporaryReferenceSet: () => unknown;
|
||||
basename?: string;
|
||||
decodeReply?: DecodeReplyFunction;
|
||||
decodeAction?: DecodeActionFunction;
|
||||
decodeFormState?: DecodeFormStateFunction;
|
||||
requestContext?: RouterContextProvider;
|
||||
loadServerAction?: LoadServerActionFunction;
|
||||
onError?: (error: unknown) => void;
|
||||
request: Request;
|
||||
routes: RSCRouteConfigEntry[];
|
||||
routeDiscovery?: RouteDiscovery;
|
||||
generateResponse: (match: RSCMatch, { onError, temporaryReferences, }: {
|
||||
onError(error: unknown): string | undefined;
|
||||
temporaryReferences: unknown;
|
||||
}) => Response;
|
||||
}): Promise<Response>;
|
||||
|
||||
type BrowserCreateFromReadableStreamFunction = (body: ReadableStream<Uint8Array>, { temporaryReferences, }: {
|
||||
temporaryReferences: unknown;
|
||||
}) => Promise<unknown>;
|
||||
type EncodeReplyFunction = (args: unknown[], options: {
|
||||
temporaryReferences: unknown;
|
||||
}) => Promise<BodyInit>;
|
||||
/**
|
||||
* Create a React `callServer` implementation for React Router.
|
||||
*
|
||||
* @example
|
||||
* import {
|
||||
* createFromReadableStream,
|
||||
* createTemporaryReferenceSet,
|
||||
* encodeReply,
|
||||
* setServerCallback,
|
||||
* } from "@vitejs/plugin-rsc/browser";
|
||||
* import { unstable_createCallServer as createCallServer } from "react-router";
|
||||
*
|
||||
* setServerCallback(
|
||||
* createCallServer({
|
||||
* createFromReadableStream,
|
||||
* createTemporaryReferenceSet,
|
||||
* encodeReply,
|
||||
* })
|
||||
* );
|
||||
*
|
||||
* @name unstable_createCallServer
|
||||
* @public
|
||||
* @category RSC
|
||||
* @mode data
|
||||
* @param opts Options
|
||||
* @param opts.createFromReadableStream Your `react-server-dom-xyz/client`'s
|
||||
* `createFromReadableStream`. Used to decode payloads from the server.
|
||||
* @param opts.createTemporaryReferenceSet A function that creates a temporary
|
||||
* reference set for the [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* payload.
|
||||
* @param opts.encodeReply Your `react-server-dom-xyz/client`'s `encodeReply`.
|
||||
* Used when sending payloads to the server.
|
||||
* @param opts.fetch Optional [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
|
||||
* implementation. Defaults to global [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch).
|
||||
* @returns A function that can be used to call server actions.
|
||||
*/
|
||||
declare function createCallServer({ createFromReadableStream, createTemporaryReferenceSet, encodeReply, fetch: fetchImplementation, }: {
|
||||
createFromReadableStream: BrowserCreateFromReadableStreamFunction;
|
||||
createTemporaryReferenceSet: () => unknown;
|
||||
encodeReply: EncodeReplyFunction;
|
||||
fetch?: (request: Request) => Promise<Response>;
|
||||
}): (id: string, args: unknown[]) => Promise<unknown>;
|
||||
/**
|
||||
* Props for the {@link unstable_RSCHydratedRouter} component.
|
||||
*
|
||||
* @name unstable_RSCHydratedRouterProps
|
||||
* @category Types
|
||||
*/
|
||||
interface RSCHydratedRouterProps {
|
||||
/**
|
||||
* Your `react-server-dom-xyz/client`'s `createFromReadableStream` function,
|
||||
* used to decode payloads from the server.
|
||||
*/
|
||||
createFromReadableStream: BrowserCreateFromReadableStreamFunction;
|
||||
/**
|
||||
* Optional fetch implementation. Defaults to global [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch).
|
||||
*/
|
||||
fetch?: (request: Request) => Promise<Response>;
|
||||
/**
|
||||
* The decoded {@link unstable_RSCPayload} to hydrate.
|
||||
*/
|
||||
payload: RSCPayload;
|
||||
/**
|
||||
* A function that returns an {@link RouterContextProvider} instance
|
||||
* which is provided as the `context` argument to client [`action`](../../start/data/route-object#action)s,
|
||||
* [`loader`](../../start/data/route-object#loader)s and [middleware](../../how-to/middleware).
|
||||
* This function is called to generate a fresh `context` instance on each
|
||||
* navigation or fetcher call.
|
||||
*/
|
||||
getContext?: RouterInit["getContext"];
|
||||
}
|
||||
/**
|
||||
* Hydrates a server rendered {@link unstable_RSCPayload} in the browser.
|
||||
*
|
||||
* @example
|
||||
* import { startTransition, StrictMode } from "react";
|
||||
* import { hydrateRoot } from "react-dom/client";
|
||||
* import {
|
||||
* unstable_getRSCStream as getRSCStream,
|
||||
* unstable_RSCHydratedRouter as RSCHydratedRouter,
|
||||
* } from "react-router";
|
||||
* import type { unstable_RSCPayload as RSCPayload } from "react-router";
|
||||
*
|
||||
* createFromReadableStream(getRSCStream()).then((payload) =>
|
||||
* startTransition(async () => {
|
||||
* hydrateRoot(
|
||||
* document,
|
||||
* <StrictMode>
|
||||
* <RSCHydratedRouter
|
||||
* createFromReadableStream={createFromReadableStream}
|
||||
* payload={payload}
|
||||
* />
|
||||
* </StrictMode>,
|
||||
* { formState: await getFormState(payload) },
|
||||
* );
|
||||
* }),
|
||||
* );
|
||||
*
|
||||
* @name unstable_RSCHydratedRouter
|
||||
* @public
|
||||
* @category RSC
|
||||
* @mode data
|
||||
* @param props Props
|
||||
* @param {unstable_RSCHydratedRouterProps.createFromReadableStream} props.createFromReadableStream n/a
|
||||
* @param {unstable_RSCHydratedRouterProps.fetch} props.fetch n/a
|
||||
* @param {unstable_RSCHydratedRouterProps.getContext} props.getContext n/a
|
||||
* @param {unstable_RSCHydratedRouterProps.payload} props.payload n/a
|
||||
* @returns A hydrated {@link DataRouter} that can be used to navigate and
|
||||
* render routes.
|
||||
*/
|
||||
declare function RSCHydratedRouter({ createFromReadableStream, fetch: fetchImplementation, payload, getContext, }: RSCHydratedRouterProps): React.JSX.Element;
|
||||
|
||||
export { type BrowserCreateFromReadableStreamFunction as B, type DecodeActionFunction as D, type EncodeReplyFunction as E, type LoadServerActionFunction as L, RSCHydratedRouter as R, type DecodeFormStateFunction as a, type DecodeReplyFunction as b, createCallServer as c, type RSCManifestPayload as d, type RSCPayload as e, type RSCRenderPayload as f, getRequest as g, type RSCHydratedRouterProps as h, type RSCMatch as i, type RSCRouteManifest as j, type RSCRouteMatch as k, type RSCRouteConfigEntry as l, matchRSCServerRequest as m, type RSCRouteConfig as n };
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }/**
|
||||
* react-router v7.18.0
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var _chunkIUPBOWYOjs = require('./chunk-IUPBOWYO.js');
|
||||
|
||||
// lib/dom/ssr/hydration.tsx
|
||||
function getHydrationData({
|
||||
state,
|
||||
routes,
|
||||
getRouteInfo,
|
||||
location,
|
||||
basename,
|
||||
isSpaMode
|
||||
}) {
|
||||
let hydrationData = {
|
||||
...state,
|
||||
loaderData: { ...state.loaderData }
|
||||
};
|
||||
let initialMatches = _chunkIUPBOWYOjs.matchRoutes.call(void 0, routes, location, basename);
|
||||
if (initialMatches) {
|
||||
for (let match of initialMatches) {
|
||||
let routeId = match.route.id;
|
||||
let routeInfo = getRouteInfo(routeId);
|
||||
if (_chunkIUPBOWYOjs.shouldHydrateRouteLoader.call(void 0,
|
||||
routeId,
|
||||
routeInfo.clientLoader,
|
||||
routeInfo.hasLoader,
|
||||
isSpaMode
|
||||
) && (routeInfo.hasHydrateFallback || !routeInfo.hasLoader)) {
|
||||
delete hydrationData.loaderData[routeId];
|
||||
} else if (!routeInfo.hasLoader) {
|
||||
hydrationData.loaderData[routeId] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return hydrationData;
|
||||
}
|
||||
|
||||
// lib/rsc/errorBoundaries.tsx
|
||||
var _react = require('react'); var _react2 = _interopRequireDefault(_react);
|
||||
var RSCRouterGlobalErrorBoundary = class extends _react2.default.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { error: null, location: props.location };
|
||||
}
|
||||
static getDerivedStateFromError(error) {
|
||||
return { error };
|
||||
}
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
if (state.location !== props.location) {
|
||||
return { error: null, location: props.location };
|
||||
}
|
||||
return { error: state.error, location: state.location };
|
||||
}
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return /* @__PURE__ */ _react2.default.createElement(
|
||||
RSCDefaultRootErrorBoundaryImpl,
|
||||
{
|
||||
error: this.state.error,
|
||||
renderAppShell: true
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
};
|
||||
function ErrorWrapper({
|
||||
renderAppShell,
|
||||
title,
|
||||
children
|
||||
}) {
|
||||
if (!renderAppShell) {
|
||||
return children;
|
||||
}
|
||||
return /* @__PURE__ */ _react2.default.createElement("html", { lang: "en" }, /* @__PURE__ */ _react2.default.createElement("head", null, /* @__PURE__ */ _react2.default.createElement("meta", { charSet: "utf-8" }), /* @__PURE__ */ _react2.default.createElement(
|
||||
"meta",
|
||||
{
|
||||
name: "viewport",
|
||||
content: "width=device-width,initial-scale=1,viewport-fit=cover"
|
||||
}
|
||||
), /* @__PURE__ */ _react2.default.createElement("title", null, title)), /* @__PURE__ */ _react2.default.createElement("body", null, /* @__PURE__ */ _react2.default.createElement("main", { style: { fontFamily: "system-ui, sans-serif", padding: "2rem" } }, children)));
|
||||
}
|
||||
function RSCDefaultRootErrorBoundaryImpl({
|
||||
error,
|
||||
renderAppShell
|
||||
}) {
|
||||
console.error(error);
|
||||
let heyDeveloper = /* @__PURE__ */ _react2.default.createElement(
|
||||
"script",
|
||||
{
|
||||
dangerouslySetInnerHTML: {
|
||||
__html: `
|
||||
console.log(
|
||||
"\u{1F4BF} Hey developer \u{1F44B}. You can provide a way better UX than this when your app throws errors. Check out https://reactrouter.com/how-to/error-boundary for more information."
|
||||
);
|
||||
`
|
||||
}
|
||||
}
|
||||
);
|
||||
if (_chunkIUPBOWYOjs.isRouteErrorResponse.call(void 0, error)) {
|
||||
return /* @__PURE__ */ _react2.default.createElement(
|
||||
ErrorWrapper,
|
||||
{
|
||||
renderAppShell,
|
||||
title: "Unhandled Thrown Response!"
|
||||
},
|
||||
/* @__PURE__ */ _react2.default.createElement("h1", { style: { fontSize: "24px" } }, error.status, " ", error.statusText),
|
||||
_chunkIUPBOWYOjs.ENABLE_DEV_WARNINGS ? heyDeveloper : null
|
||||
);
|
||||
}
|
||||
let errorInstance;
|
||||
if (error instanceof Error) {
|
||||
errorInstance = error;
|
||||
} else {
|
||||
let errorString = error == null ? "Unknown Error" : typeof error === "object" && "toString" in error ? error.toString() : JSON.stringify(error);
|
||||
errorInstance = new Error(errorString);
|
||||
}
|
||||
return /* @__PURE__ */ _react2.default.createElement(ErrorWrapper, { renderAppShell, title: "Application Error!" }, /* @__PURE__ */ _react2.default.createElement("h1", { style: { fontSize: "24px" } }, "Application Error"), /* @__PURE__ */ _react2.default.createElement(
|
||||
"pre",
|
||||
{
|
||||
style: {
|
||||
padding: "2rem",
|
||||
background: "hsla(10, 50%, 50%, 0.1)",
|
||||
color: "red",
|
||||
overflow: "auto"
|
||||
}
|
||||
},
|
||||
errorInstance.stack
|
||||
), heyDeveloper);
|
||||
}
|
||||
function RSCDefaultRootErrorBoundary({
|
||||
hasRootLayout
|
||||
}) {
|
||||
let error = _chunkIUPBOWYOjs.useRouteError.call(void 0, );
|
||||
if (hasRootLayout === void 0) {
|
||||
throw new Error("Missing 'hasRootLayout' prop");
|
||||
}
|
||||
return /* @__PURE__ */ _react2.default.createElement(
|
||||
RSCDefaultRootErrorBoundaryImpl,
|
||||
{
|
||||
renderAppShell: !hasRootLayout,
|
||||
error
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// lib/rsc/route-modules.ts
|
||||
function createRSCRouteModules(payload) {
|
||||
const routeModules = {};
|
||||
for (const match of payload.matches) {
|
||||
populateRSCRouteModules(routeModules, match);
|
||||
}
|
||||
return routeModules;
|
||||
}
|
||||
function populateRSCRouteModules(routeModules, matches) {
|
||||
matches = Array.isArray(matches) ? matches : [matches];
|
||||
for (const match of matches) {
|
||||
routeModules[match.id] = {
|
||||
links: match.links,
|
||||
meta: match.meta,
|
||||
default: noopComponent
|
||||
};
|
||||
}
|
||||
}
|
||||
var noopComponent = () => null;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
exports.getHydrationData = getHydrationData; exports.RSCRouterGlobalErrorBoundary = RSCRouterGlobalErrorBoundary; exports.RSCDefaultRootErrorBoundary = RSCDefaultRootErrorBoundary; exports.createRSCRouteModules = createRSCRouteModules; exports.populateRSCRouteModules = populateRSCRouteModules;
|
||||
+1366
File diff suppressed because it is too large
Load Diff
+2517
File diff suppressed because it is too large
Load Diff
+10307
File diff suppressed because one or more lines are too long
+11606
File diff suppressed because it is too large
Load Diff
+1779
File diff suppressed because it is too large
Load Diff
+1740
File diff suppressed because it is too large
Load Diff
+1740
File diff suppressed because it is too large
Load Diff
+172
@@ -0,0 +1,172 @@
|
||||
import * as React from 'react';
|
||||
import { a as RouterProviderProps$1, R as RouterInit, C as ClientInstrumentation, b as ClientOnErrorFunction } from './context-CeD5LmaF.mjs';
|
||||
export { D as unstable_DecodeActionFunction, a as unstable_DecodeFormStateFunction, b as unstable_DecodeReplyFunction, R as unstable_RSCHydratedRouter, d as unstable_RSCManifestPayload, e as unstable_RSCPayload, f as unstable_RSCRenderPayload, c as unstable_createCallServer } from './browser-DBmQ1yAR.mjs';
|
||||
import './data-DEjBmEfD.mjs';
|
||||
|
||||
type RouterProviderProps = Omit<RouterProviderProps$1, "flushSync">;
|
||||
declare function RouterProvider(props: RouterProviderProps): React.JSX.Element;
|
||||
|
||||
/**
|
||||
* Props for the {@link dom.HydratedRouter} component.
|
||||
*
|
||||
* @category Types
|
||||
*/
|
||||
interface HydratedRouterProps {
|
||||
/**
|
||||
* Context factory function to be passed through to {@link createBrowserRouter}.
|
||||
* This function will be called to create a fresh `context` instance on each
|
||||
* navigation/fetch and made available to
|
||||
* [`clientAction`](../../start/framework/route-module#clientAction)/[`clientLoader`](../../start/framework/route-module#clientLoader)
|
||||
* functions.
|
||||
*/
|
||||
getContext?: RouterInit["getContext"];
|
||||
/**
|
||||
* Array of instrumentation objects allowing you to instrument the router and
|
||||
* individual routes prior to router initialization (and on any subsequently
|
||||
* added routes via `route.lazy` or `patchRoutesOnNavigation`). This is
|
||||
* mostly useful for observability such as wrapping navigations, fetches,
|
||||
* as well as route loaders/actions/middlewares with logging and/or performance
|
||||
* tracing. See the [docs](../../how-to/instrumentation) for more information.
|
||||
*
|
||||
* ```tsx
|
||||
* const logging = {
|
||||
* router({ instrument }) {
|
||||
* instrument({
|
||||
* navigate: (impl, { to }) => logExecution(`navigate ${to}`, impl),
|
||||
* fetch: (impl, { to }) => logExecution(`fetch ${to}`, impl)
|
||||
* });
|
||||
* },
|
||||
* route({ instrument, id }) {
|
||||
* instrument({
|
||||
* middleware: (impl, { request }) => logExecution(
|
||||
* `middleware ${request.url} (route ${id})`,
|
||||
* impl
|
||||
* ),
|
||||
* loader: (impl, { request }) => logExecution(
|
||||
* `loader ${request.url} (route ${id})`,
|
||||
* impl
|
||||
* ),
|
||||
* action: (impl, { request }) => logExecution(
|
||||
* `action ${request.url} (route ${id})`,
|
||||
* impl
|
||||
* ),
|
||||
* })
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* async function logExecution(label: string, impl: () => Promise<void>) {
|
||||
* let start = performance.now();
|
||||
* console.log(`start ${label}`);
|
||||
* await impl();
|
||||
* let duration = Math.round(performance.now() - start);
|
||||
* console.log(`end ${label} (${duration}ms)`);
|
||||
* }
|
||||
*
|
||||
* startTransition(() => {
|
||||
* hydrateRoot(
|
||||
* document,
|
||||
* <HydratedRouter instrumentations={[logging]} />
|
||||
* );
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
instrumentations?: ClientInstrumentation[];
|
||||
/**
|
||||
* An error handler function that will be called for any middleware, loader, action,
|
||||
* or render errors that are encountered in your application. This is useful for
|
||||
* logging or reporting errors instead of in the {@link ErrorBoundary} because it's not
|
||||
* subject to re-rendering and will only run one time per error.
|
||||
*
|
||||
* The `errorInfo` parameter is passed along from
|
||||
* [`componentDidCatch`](https://react.dev/reference/react/Component#componentdidcatch)
|
||||
* and is only present for render errors.
|
||||
*
|
||||
* ```tsx
|
||||
* <HydratedRouter onError={(error, info) => {
|
||||
* let { location, params, pattern, errorInfo } = info;
|
||||
* console.error(error, location, errorInfo);
|
||||
* reportToErrorService(error, location, errorInfo);
|
||||
* }} />
|
||||
* ```
|
||||
*/
|
||||
onError?: ClientOnErrorFunction;
|
||||
/**
|
||||
* Control whether router state updates are internally wrapped in
|
||||
* [`React.startTransition`](https://react.dev/reference/react/startTransition).
|
||||
*
|
||||
* - When left `undefined`, all state updates are wrapped in
|
||||
* `React.startTransition`
|
||||
* - This can lead to buggy behaviors if you are wrapping your own
|
||||
* navigations/fetchers in `startTransition`.
|
||||
* - When set to `true`, {@link Link} and {@link Form} navigations will be wrapped
|
||||
* in `React.startTransition` and router state changes will be wrapped in
|
||||
* `React.startTransition` and also sent through
|
||||
* [`useOptimistic`](https://react.dev/reference/react/useOptimistic) to
|
||||
* surface mid-navigation router state changes to the UI.
|
||||
* - When set to `false`, the router will not leverage `React.startTransition` or
|
||||
* `React.useOptimistic` on any navigations or state changes.
|
||||
*
|
||||
* For more information, please see the [docs](../../explanation/react-transitions).
|
||||
*/
|
||||
useTransitions?: boolean;
|
||||
}
|
||||
/**
|
||||
* Framework-mode router component to be used to hydrate a router from a
|
||||
* {@link ServerRouter}. See [`entry.client.tsx`](../framework-conventions/entry.client.tsx).
|
||||
*
|
||||
* @public
|
||||
* @category Framework Routers
|
||||
* @mode framework
|
||||
* @param props Props
|
||||
* @param {dom.HydratedRouterProps.getContext} props.getContext n/a
|
||||
* @param {dom.HydratedRouterProps.onError} props.onError n/a
|
||||
* @returns A React element that represents the hydrated application.
|
||||
*/
|
||||
declare function HydratedRouter(props: HydratedRouterProps): React.JSX.Element;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__FLIGHT_DATA: any[];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the prerendered [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* stream for hydration. Usually passed directly to your
|
||||
* `react-server-dom-xyz/client`'s `createFromReadableStream`.
|
||||
*
|
||||
* @example
|
||||
* import { startTransition, StrictMode } from "react";
|
||||
* import { hydrateRoot } from "react-dom/client";
|
||||
* import {
|
||||
* unstable_getRSCStream as getRSCStream,
|
||||
* unstable_RSCHydratedRouter as RSCHydratedRouter,
|
||||
* } from "react-router";
|
||||
* import type { unstable_RSCPayload as RSCPayload } from "react-router";
|
||||
*
|
||||
* createFromReadableStream(getRSCStream()).then(
|
||||
* (payload: RSCServerPayload) => {
|
||||
* startTransition(async () => {
|
||||
* hydrateRoot(
|
||||
* document,
|
||||
* <StrictMode>
|
||||
* <RSCHydratedRouter {...props} />
|
||||
* </StrictMode>,
|
||||
* {
|
||||
* // Options
|
||||
* }
|
||||
* );
|
||||
* });
|
||||
* }
|
||||
* );
|
||||
*
|
||||
* @name unstable_getRSCStream
|
||||
* @public
|
||||
* @category RSC
|
||||
* @mode data
|
||||
* @returns A [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
|
||||
* that contains the [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* data for hydration.
|
||||
*/
|
||||
declare function getRSCStream(): ReadableStream;
|
||||
|
||||
export { HydratedRouter, type HydratedRouterProps, RouterProvider, type RouterProviderProps, getRSCStream as unstable_getRSCStream };
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
import * as React from 'react';
|
||||
import { RouterProviderProps as RouterProviderProps$1, RouterInit, ClientOnErrorFunction } from 'react-router';
|
||||
import { C as ClientInstrumentation } from './instrumentation-Dkmpzd13.js';
|
||||
export { D as unstable_DecodeActionFunction, a as unstable_DecodeFormStateFunction, b as unstable_DecodeReplyFunction, R as unstable_RSCHydratedRouter, d as unstable_RSCManifestPayload, e as unstable_RSCPayload, f as unstable_RSCRenderPayload, c as unstable_createCallServer } from './browser-B2PdsXXH.js';
|
||||
import './data-CjO11-hU.js';
|
||||
|
||||
type RouterProviderProps = Omit<RouterProviderProps$1, "flushSync">;
|
||||
declare function RouterProvider(props: RouterProviderProps): React.JSX.Element;
|
||||
|
||||
/**
|
||||
* Props for the {@link dom.HydratedRouter} component.
|
||||
*
|
||||
* @category Types
|
||||
*/
|
||||
interface HydratedRouterProps {
|
||||
/**
|
||||
* Context factory function to be passed through to {@link createBrowserRouter}.
|
||||
* This function will be called to create a fresh `context` instance on each
|
||||
* navigation/fetch and made available to
|
||||
* [`clientAction`](../../start/framework/route-module#clientAction)/[`clientLoader`](../../start/framework/route-module#clientLoader)
|
||||
* functions.
|
||||
*/
|
||||
getContext?: RouterInit["getContext"];
|
||||
/**
|
||||
* Array of instrumentation objects allowing you to instrument the router and
|
||||
* individual routes prior to router initialization (and on any subsequently
|
||||
* added routes via `route.lazy` or `patchRoutesOnNavigation`). This is
|
||||
* mostly useful for observability such as wrapping navigations, fetches,
|
||||
* as well as route loaders/actions/middlewares with logging and/or performance
|
||||
* tracing. See the [docs](../../how-to/instrumentation) for more information.
|
||||
*
|
||||
* ```tsx
|
||||
* const logging = {
|
||||
* router({ instrument }) {
|
||||
* instrument({
|
||||
* navigate: (impl, { to }) => logExecution(`navigate ${to}`, impl),
|
||||
* fetch: (impl, { to }) => logExecution(`fetch ${to}`, impl)
|
||||
* });
|
||||
* },
|
||||
* route({ instrument, id }) {
|
||||
* instrument({
|
||||
* middleware: (impl, { request }) => logExecution(
|
||||
* `middleware ${request.url} (route ${id})`,
|
||||
* impl
|
||||
* ),
|
||||
* loader: (impl, { request }) => logExecution(
|
||||
* `loader ${request.url} (route ${id})`,
|
||||
* impl
|
||||
* ),
|
||||
* action: (impl, { request }) => logExecution(
|
||||
* `action ${request.url} (route ${id})`,
|
||||
* impl
|
||||
* ),
|
||||
* })
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* async function logExecution(label: string, impl: () => Promise<void>) {
|
||||
* let start = performance.now();
|
||||
* console.log(`start ${label}`);
|
||||
* await impl();
|
||||
* let duration = Math.round(performance.now() - start);
|
||||
* console.log(`end ${label} (${duration}ms)`);
|
||||
* }
|
||||
*
|
||||
* startTransition(() => {
|
||||
* hydrateRoot(
|
||||
* document,
|
||||
* <HydratedRouter instrumentations={[logging]} />
|
||||
* );
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
instrumentations?: ClientInstrumentation[];
|
||||
/**
|
||||
* An error handler function that will be called for any middleware, loader, action,
|
||||
* or render errors that are encountered in your application. This is useful for
|
||||
* logging or reporting errors instead of in the {@link ErrorBoundary} because it's not
|
||||
* subject to re-rendering and will only run one time per error.
|
||||
*
|
||||
* The `errorInfo` parameter is passed along from
|
||||
* [`componentDidCatch`](https://react.dev/reference/react/Component#componentdidcatch)
|
||||
* and is only present for render errors.
|
||||
*
|
||||
* ```tsx
|
||||
* <HydratedRouter onError={(error, info) => {
|
||||
* let { location, params, pattern, errorInfo } = info;
|
||||
* console.error(error, location, errorInfo);
|
||||
* reportToErrorService(error, location, errorInfo);
|
||||
* }} />
|
||||
* ```
|
||||
*/
|
||||
onError?: ClientOnErrorFunction;
|
||||
/**
|
||||
* Control whether router state updates are internally wrapped in
|
||||
* [`React.startTransition`](https://react.dev/reference/react/startTransition).
|
||||
*
|
||||
* - When left `undefined`, all state updates are wrapped in
|
||||
* `React.startTransition`
|
||||
* - This can lead to buggy behaviors if you are wrapping your own
|
||||
* navigations/fetchers in `startTransition`.
|
||||
* - When set to `true`, {@link Link} and {@link Form} navigations will be wrapped
|
||||
* in `React.startTransition` and router state changes will be wrapped in
|
||||
* `React.startTransition` and also sent through
|
||||
* [`useOptimistic`](https://react.dev/reference/react/useOptimistic) to
|
||||
* surface mid-navigation router state changes to the UI.
|
||||
* - When set to `false`, the router will not leverage `React.startTransition` or
|
||||
* `React.useOptimistic` on any navigations or state changes.
|
||||
*
|
||||
* For more information, please see the [docs](../../explanation/react-transitions).
|
||||
*/
|
||||
useTransitions?: boolean;
|
||||
}
|
||||
/**
|
||||
* Framework-mode router component to be used to hydrate a router from a
|
||||
* {@link ServerRouter}. See [`entry.client.tsx`](../framework-conventions/entry.client.tsx).
|
||||
*
|
||||
* @public
|
||||
* @category Framework Routers
|
||||
* @mode framework
|
||||
* @param props Props
|
||||
* @param {dom.HydratedRouterProps.getContext} props.getContext n/a
|
||||
* @param {dom.HydratedRouterProps.onError} props.onError n/a
|
||||
* @returns A React element that represents the hydrated application.
|
||||
*/
|
||||
declare function HydratedRouter(props: HydratedRouterProps): React.JSX.Element;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__FLIGHT_DATA: any[];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the prerendered [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* stream for hydration. Usually passed directly to your
|
||||
* `react-server-dom-xyz/client`'s `createFromReadableStream`.
|
||||
*
|
||||
* @example
|
||||
* import { startTransition, StrictMode } from "react";
|
||||
* import { hydrateRoot } from "react-dom/client";
|
||||
* import {
|
||||
* unstable_getRSCStream as getRSCStream,
|
||||
* unstable_RSCHydratedRouter as RSCHydratedRouter,
|
||||
* } from "react-router";
|
||||
* import type { unstable_RSCPayload as RSCPayload } from "react-router";
|
||||
*
|
||||
* createFromReadableStream(getRSCStream()).then(
|
||||
* (payload: RSCServerPayload) => {
|
||||
* startTransition(async () => {
|
||||
* hydrateRoot(
|
||||
* document,
|
||||
* <StrictMode>
|
||||
* <RSCHydratedRouter {...props} />
|
||||
* </StrictMode>,
|
||||
* {
|
||||
* // Options
|
||||
* }
|
||||
* );
|
||||
* });
|
||||
* }
|
||||
* );
|
||||
*
|
||||
* @name unstable_getRSCStream
|
||||
* @public
|
||||
* @category RSC
|
||||
* @mode data
|
||||
* @returns A [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
|
||||
* that contains the [RSC](https://react.dev/reference/rsc/server-components)
|
||||
* data for hydration.
|
||||
*/
|
||||
declare function getRSCStream(): ReadableStream;
|
||||
|
||||
export { HydratedRouter, type HydratedRouterProps, RouterProvider, type RouterProviderProps, getRSCStream as unstable_getRSCStream };
|
||||
+1018
File diff suppressed because it is too large
Load Diff
+1010
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+3677
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+2614
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
export { Q as MemoryRouter, T as Navigate, U as Outlet, V as Route, W as Router, X as RouterProvider, Y as Routes, A as UNSAFE_AwaitContextProvider, ab as UNSAFE_WithComponentProps, af as UNSAFE_WithErrorBoundaryProps, ad as UNSAFE_WithHydrateFallbackProps } from './context-CeD5LmaF.mjs';
|
||||
export { l as BrowserRouter, q as Form, m as HashRouter, n as Link, X as Links, W as Meta, p as NavLink, r as ScrollRestoration, T as StaticRouter, V as StaticRouterProvider, o as unstable_HistoryRouter } from './index-react-server-client-CACgcj2J.mjs';
|
||||
import './data-DEjBmEfD.mjs';
|
||||
import 'react';
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
export { W as BrowserRouter, $ as Form, X as HashRouter, Y as Link, an as Links, j as MemoryRouter, am as Meta, _ as NavLink, k as Navigate, l as Outlet, m as Route, n as Router, o as RouterProvider, p as Routes, a0 as ScrollRestoration, ak as StaticRouter, al as StaticRouterProvider, b as UNSAFE_AwaitContextProvider, aH as UNSAFE_WithComponentProps, aL as UNSAFE_WithErrorBoundaryProps, aJ as UNSAFE_WithHydrateFallbackProps, Z as unstable_HistoryRouter } from './index-react-server-client-3ykjivgQ.js';
|
||||
import './instrumentation-Dkmpzd13.js';
|
||||
import './data-CjO11-hU.js';
|
||||
import 'react';
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
"use strict";Object.defineProperty(exports, "__esModule", {value: true});/**
|
||||
* react-router v7.18.0
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
"use client";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var _chunkBIP66BKVjs = require('./chunk-BIP66BKV.js');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var _chunkIUPBOWYOjs = require('./chunk-IUPBOWYO.js');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
exports.BrowserRouter = _chunkBIP66BKVjs.BrowserRouter; exports.Form = _chunkBIP66BKVjs.Form; exports.HashRouter = _chunkBIP66BKVjs.HashRouter; exports.Link = _chunkBIP66BKVjs.Link; exports.Links = _chunkIUPBOWYOjs.Links; exports.MemoryRouter = _chunkIUPBOWYOjs.MemoryRouter; exports.Meta = _chunkIUPBOWYOjs.Meta; exports.NavLink = _chunkBIP66BKVjs.NavLink; exports.Navigate = _chunkIUPBOWYOjs.Navigate; exports.Outlet = _chunkIUPBOWYOjs.Outlet; exports.Route = _chunkIUPBOWYOjs.Route; exports.Router = _chunkIUPBOWYOjs.Router; exports.RouterProvider = _chunkIUPBOWYOjs.RouterProvider; exports.Routes = _chunkIUPBOWYOjs.Routes; exports.ScrollRestoration = _chunkBIP66BKVjs.ScrollRestoration; exports.StaticRouter = _chunkBIP66BKVjs.StaticRouter; exports.StaticRouterProvider = _chunkBIP66BKVjs.StaticRouterProvider; exports.UNSAFE_AwaitContextProvider = _chunkIUPBOWYOjs.AwaitContextProvider; exports.UNSAFE_WithComponentProps = _chunkIUPBOWYOjs.WithComponentProps; exports.UNSAFE_WithErrorBoundaryProps = _chunkIUPBOWYOjs.WithErrorBoundaryProps; exports.UNSAFE_WithHydrateFallbackProps = _chunkIUPBOWYOjs.WithHydrateFallbackProps; exports.unstable_HistoryRouter = _chunkBIP66BKVjs.HistoryRouter;
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* react-router v7.18.0
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
"use client";
|
||||
import {
|
||||
AwaitContextProvider,
|
||||
BrowserRouter,
|
||||
Form,
|
||||
HashRouter,
|
||||
HistoryRouter,
|
||||
Link,
|
||||
Links,
|
||||
MemoryRouter,
|
||||
Meta,
|
||||
NavLink,
|
||||
Navigate,
|
||||
Outlet,
|
||||
Route,
|
||||
Router,
|
||||
RouterProvider,
|
||||
Routes,
|
||||
ScrollRestoration,
|
||||
StaticRouter,
|
||||
StaticRouterProvider,
|
||||
WithComponentProps,
|
||||
WithErrorBoundaryProps,
|
||||
WithHydrateFallbackProps
|
||||
} from "./chunk-M7NGGUU6.mjs";
|
||||
export {
|
||||
BrowserRouter,
|
||||
Form,
|
||||
HashRouter,
|
||||
Link,
|
||||
Links,
|
||||
MemoryRouter,
|
||||
Meta,
|
||||
NavLink,
|
||||
Navigate,
|
||||
Outlet,
|
||||
Route,
|
||||
Router,
|
||||
RouterProvider,
|
||||
Routes,
|
||||
ScrollRestoration,
|
||||
StaticRouter,
|
||||
StaticRouterProvider,
|
||||
AwaitContextProvider as UNSAFE_AwaitContextProvider,
|
||||
WithComponentProps as UNSAFE_WithComponentProps,
|
||||
WithErrorBoundaryProps as UNSAFE_WithErrorBoundaryProps,
|
||||
WithHydrateFallbackProps as UNSAFE_WithHydrateFallbackProps,
|
||||
HistoryRouter as unstable_HistoryRouter
|
||||
};
|
||||
+2711
File diff suppressed because it is too large
Load Diff
+2711
File diff suppressed because it is too large
Load Diff
+3915
File diff suppressed because it is too large
Load Diff
+3803
File diff suppressed because it is too large
Load Diff
+1479
File diff suppressed because it is too large
Load Diff
+1479
File diff suppressed because it is too large
Load Diff
+2562
File diff suppressed because one or more lines are too long
+275
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* react-router v7.18.0
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
"use client";
|
||||
import {
|
||||
RSCDefaultRootErrorBoundary,
|
||||
RSCStaticRouter,
|
||||
ServerMode,
|
||||
ServerRouter,
|
||||
createCookie,
|
||||
createCookieSessionStorage,
|
||||
createMemorySessionStorage,
|
||||
createRequestHandler,
|
||||
createRoutesStub,
|
||||
createSession,
|
||||
createSessionStorage,
|
||||
getHydrationData,
|
||||
href,
|
||||
isCookie,
|
||||
isSession,
|
||||
routeRSCServerRequest,
|
||||
setDevServerHooks
|
||||
} from "./chunk-EGOTSXNH.mjs";
|
||||
import {
|
||||
Action,
|
||||
Await,
|
||||
AwaitContextProvider,
|
||||
BrowserRouter,
|
||||
DataRouterContext,
|
||||
DataRouterStateContext,
|
||||
ErrorResponseImpl,
|
||||
FetchersContext,
|
||||
Form,
|
||||
FrameworkContext,
|
||||
HashRouter,
|
||||
HistoryRouter,
|
||||
IDLE_BLOCKER,
|
||||
IDLE_FETCHER,
|
||||
IDLE_NAVIGATION,
|
||||
Link,
|
||||
Links,
|
||||
LocationContext,
|
||||
MemoryRouter,
|
||||
Meta,
|
||||
NavLink,
|
||||
Navigate,
|
||||
NavigationContext,
|
||||
Outlet,
|
||||
PrefetchPageLinks,
|
||||
RemixErrorBoundary,
|
||||
Route,
|
||||
RouteContext,
|
||||
Router,
|
||||
RouterContextProvider,
|
||||
RouterProvider,
|
||||
Routes,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
SingleFetchRedirectSymbol,
|
||||
StaticRouter,
|
||||
StaticRouterProvider,
|
||||
ViewTransitionContext,
|
||||
WithComponentProps,
|
||||
WithErrorBoundaryProps,
|
||||
WithHydrateFallbackProps,
|
||||
createBrowserHistory,
|
||||
createBrowserRouter,
|
||||
createClientRoutes,
|
||||
createClientRoutesWithHMRRevalidationOptOut,
|
||||
createContext,
|
||||
createHashHistory,
|
||||
createHashRouter,
|
||||
createMemoryHistory,
|
||||
createMemoryRouter,
|
||||
createPath,
|
||||
createRouter,
|
||||
createRoutesFromChildren,
|
||||
createRoutesFromElements,
|
||||
createSearchParams,
|
||||
createStaticHandler2 as createStaticHandler,
|
||||
createStaticRouter,
|
||||
data,
|
||||
decodeViaTurboStream,
|
||||
generatePath,
|
||||
getPatchRoutesOnNavigationFunction,
|
||||
getTurboStreamSingleFetchDataStrategy,
|
||||
hydrationRouteProperties,
|
||||
invariant,
|
||||
isRouteErrorResponse,
|
||||
mapRouteProperties,
|
||||
matchPath,
|
||||
matchRoutes,
|
||||
parsePath,
|
||||
redirect,
|
||||
redirectDocument,
|
||||
renderMatches,
|
||||
replace,
|
||||
resolvePath,
|
||||
shouldHydrateRouteLoader,
|
||||
useActionData,
|
||||
useAsyncError,
|
||||
useAsyncValue,
|
||||
useBeforeUnload,
|
||||
useBlocker,
|
||||
useFetcher,
|
||||
useFetchers,
|
||||
useFogOFWarDiscovery,
|
||||
useFormAction,
|
||||
useHref,
|
||||
useInRouterContext,
|
||||
useLinkClickHandler,
|
||||
useLoaderData,
|
||||
useLocation,
|
||||
useMatch,
|
||||
useMatches,
|
||||
useNavigate,
|
||||
useNavigation,
|
||||
useNavigationType,
|
||||
useOutlet,
|
||||
useOutletContext,
|
||||
useParams,
|
||||
usePrompt,
|
||||
useResolvedPath,
|
||||
useRevalidator,
|
||||
useRoute,
|
||||
useRouteError,
|
||||
useRouteLoaderData,
|
||||
useRouterState,
|
||||
useRoutes,
|
||||
useScrollRestoration,
|
||||
useSearchParams,
|
||||
useSubmit,
|
||||
useViewTransitionState,
|
||||
withComponentProps,
|
||||
withErrorBoundaryProps,
|
||||
withHydrateFallbackProps
|
||||
} from "./chunk-M7NGGUU6.mjs";
|
||||
export {
|
||||
Await,
|
||||
BrowserRouter,
|
||||
Form,
|
||||
HashRouter,
|
||||
IDLE_BLOCKER,
|
||||
IDLE_FETCHER,
|
||||
IDLE_NAVIGATION,
|
||||
Link,
|
||||
Links,
|
||||
MemoryRouter,
|
||||
Meta,
|
||||
NavLink,
|
||||
Navigate,
|
||||
Action as NavigationType,
|
||||
Outlet,
|
||||
PrefetchPageLinks,
|
||||
Route,
|
||||
Router,
|
||||
RouterContextProvider,
|
||||
RouterProvider,
|
||||
Routes,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
ServerRouter,
|
||||
StaticRouter,
|
||||
StaticRouterProvider,
|
||||
AwaitContextProvider as UNSAFE_AwaitContextProvider,
|
||||
DataRouterContext as UNSAFE_DataRouterContext,
|
||||
DataRouterStateContext as UNSAFE_DataRouterStateContext,
|
||||
ErrorResponseImpl as UNSAFE_ErrorResponseImpl,
|
||||
FetchersContext as UNSAFE_FetchersContext,
|
||||
FrameworkContext as UNSAFE_FrameworkContext,
|
||||
LocationContext as UNSAFE_LocationContext,
|
||||
NavigationContext as UNSAFE_NavigationContext,
|
||||
RSCDefaultRootErrorBoundary as UNSAFE_RSCDefaultRootErrorBoundary,
|
||||
RemixErrorBoundary as UNSAFE_RemixErrorBoundary,
|
||||
RouteContext as UNSAFE_RouteContext,
|
||||
ServerMode as UNSAFE_ServerMode,
|
||||
SingleFetchRedirectSymbol as UNSAFE_SingleFetchRedirectSymbol,
|
||||
ViewTransitionContext as UNSAFE_ViewTransitionContext,
|
||||
WithComponentProps as UNSAFE_WithComponentProps,
|
||||
WithErrorBoundaryProps as UNSAFE_WithErrorBoundaryProps,
|
||||
WithHydrateFallbackProps as UNSAFE_WithHydrateFallbackProps,
|
||||
createBrowserHistory as UNSAFE_createBrowserHistory,
|
||||
createClientRoutes as UNSAFE_createClientRoutes,
|
||||
createClientRoutesWithHMRRevalidationOptOut as UNSAFE_createClientRoutesWithHMRRevalidationOptOut,
|
||||
createHashHistory as UNSAFE_createHashHistory,
|
||||
createMemoryHistory as UNSAFE_createMemoryHistory,
|
||||
createRouter as UNSAFE_createRouter,
|
||||
decodeViaTurboStream as UNSAFE_decodeViaTurboStream,
|
||||
getHydrationData as UNSAFE_getHydrationData,
|
||||
getPatchRoutesOnNavigationFunction as UNSAFE_getPatchRoutesOnNavigationFunction,
|
||||
getTurboStreamSingleFetchDataStrategy as UNSAFE_getTurboStreamSingleFetchDataStrategy,
|
||||
hydrationRouteProperties as UNSAFE_hydrationRouteProperties,
|
||||
invariant as UNSAFE_invariant,
|
||||
mapRouteProperties as UNSAFE_mapRouteProperties,
|
||||
shouldHydrateRouteLoader as UNSAFE_shouldHydrateRouteLoader,
|
||||
useFogOFWarDiscovery as UNSAFE_useFogOFWarDiscovery,
|
||||
useScrollRestoration as UNSAFE_useScrollRestoration,
|
||||
withComponentProps as UNSAFE_withComponentProps,
|
||||
withErrorBoundaryProps as UNSAFE_withErrorBoundaryProps,
|
||||
withHydrateFallbackProps as UNSAFE_withHydrateFallbackProps,
|
||||
createBrowserRouter,
|
||||
createContext,
|
||||
createCookie,
|
||||
createCookieSessionStorage,
|
||||
createHashRouter,
|
||||
createMemoryRouter,
|
||||
createMemorySessionStorage,
|
||||
createPath,
|
||||
createRequestHandler,
|
||||
createRoutesFromChildren,
|
||||
createRoutesFromElements,
|
||||
createRoutesStub,
|
||||
createSearchParams,
|
||||
createSession,
|
||||
createSessionStorage,
|
||||
createStaticHandler,
|
||||
createStaticRouter,
|
||||
data,
|
||||
generatePath,
|
||||
href,
|
||||
isCookie,
|
||||
isRouteErrorResponse,
|
||||
isSession,
|
||||
matchPath,
|
||||
matchRoutes,
|
||||
parsePath,
|
||||
redirect,
|
||||
redirectDocument,
|
||||
renderMatches,
|
||||
replace,
|
||||
resolvePath,
|
||||
HistoryRouter as unstable_HistoryRouter,
|
||||
RSCStaticRouter as unstable_RSCStaticRouter,
|
||||
routeRSCServerRequest as unstable_routeRSCServerRequest,
|
||||
setDevServerHooks as unstable_setDevServerHooks,
|
||||
usePrompt as unstable_usePrompt,
|
||||
useRoute as unstable_useRoute,
|
||||
useRouterState as unstable_useRouterState,
|
||||
useActionData,
|
||||
useAsyncError,
|
||||
useAsyncValue,
|
||||
useBeforeUnload,
|
||||
useBlocker,
|
||||
useFetcher,
|
||||
useFetchers,
|
||||
useFormAction,
|
||||
useHref,
|
||||
useInRouterContext,
|
||||
useLinkClickHandler,
|
||||
useLoaderData,
|
||||
useLocation,
|
||||
useMatch,
|
||||
useMatches,
|
||||
useNavigate,
|
||||
useNavigation,
|
||||
useNavigationType,
|
||||
useOutlet,
|
||||
useOutletContext,
|
||||
useParams,
|
||||
useResolvedPath,
|
||||
useRevalidator,
|
||||
useRouteError,
|
||||
useRouteLoaderData,
|
||||
useRoutes,
|
||||
useSearchParams,
|
||||
useSubmit,
|
||||
useViewTransitionState
|
||||
};
|
||||
+715
@@ -0,0 +1,715 @@
|
||||
import { e as RouteObject, f as History, g as MaybePromise, c as RouterContextProvider, h as MapRoutePropertiesFunction, i as Action, L as Location, D as DataRouteMatch, j as Submission, k as RouteData, l as DataStrategyFunction, m as PatchRoutesOnNavigationFunction, n as DataRouteObject, o as RouteBranch, p as RouteManifest, U as UIMatch, T as To, q as HTMLFormMethod, F as FormEncType, r as Path, s as LoaderFunctionArgs, t as MiddlewareEnabled, u as AppLoadContext } from './data-CjO11-hU.js';
|
||||
|
||||
/**
|
||||
* A Router instance manages all navigation and data loading/mutations
|
||||
*/
|
||||
interface Router {
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Return the basename for the router
|
||||
*/
|
||||
get basename(): RouterInit["basename"];
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Return the future config for the router
|
||||
*/
|
||||
get future(): FutureConfig;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Return the current state of the router
|
||||
*/
|
||||
get state(): RouterState;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Return the routes for this router instance
|
||||
*/
|
||||
get routes(): DataRouteObject[];
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Return the route branches for this router instance
|
||||
*/
|
||||
get branches(): RouteBranch<DataRouteObject>[] | undefined;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Return the manifest for this router instance
|
||||
*/
|
||||
get manifest(): RouteManifest;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Return the window associated with the router
|
||||
*/
|
||||
get window(): RouterInit["window"];
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Initialize the router, including adding history listeners and kicking off
|
||||
* initial data fetches. Returns a function to cleanup listeners and abort
|
||||
* any in-progress loads
|
||||
*/
|
||||
initialize(): Router;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Subscribe to router.state updates
|
||||
*
|
||||
* @param fn function to call with the new state
|
||||
*/
|
||||
subscribe(fn: RouterSubscriber): () => void;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Enable scroll restoration behavior in the router
|
||||
*
|
||||
* @param savedScrollPositions Object that will manage positions, in case
|
||||
* it's being restored from sessionStorage
|
||||
* @param getScrollPosition Function to get the active Y scroll position
|
||||
* @param getKey Function to get the key to use for restoration
|
||||
*/
|
||||
enableScrollRestoration(savedScrollPositions: Record<string, number>, getScrollPosition: GetScrollPositionFunction, getKey?: GetScrollRestorationKeyFunction): () => void;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Navigate forward/backward in the history stack
|
||||
* @param to Delta to move in the history stack
|
||||
*/
|
||||
navigate(to: number): Promise<void>;
|
||||
/**
|
||||
* Navigate to the given path
|
||||
* @param to Path to navigate to
|
||||
* @param opts Navigation options (method, submission, etc.)
|
||||
*/
|
||||
navigate(to: To | null, opts?: RouterNavigateOptions): Promise<void>;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Trigger a fetcher load/submission
|
||||
*
|
||||
* @param key Fetcher key
|
||||
* @param routeId Route that owns the fetcher
|
||||
* @param href href to fetch
|
||||
* @param opts Fetcher options, (method, submission, etc.)
|
||||
*/
|
||||
fetch(key: string, routeId: string, href: string | null, opts?: RouterFetchOptions): Promise<void>;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Trigger a revalidation of all current route loaders and fetcher loads
|
||||
*/
|
||||
revalidate(): Promise<void>;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Utility function to create an href for the given location
|
||||
* @param location
|
||||
*/
|
||||
createHref(location: Location | URL): string;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Utility function to URL encode a destination path according to the internal
|
||||
* history implementation
|
||||
* @param to
|
||||
*/
|
||||
encodeLocation(to: To): Path;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Get/create a fetcher for the given key
|
||||
* @param key
|
||||
*/
|
||||
getFetcher<TData = any>(key: string): Fetcher<TData>;
|
||||
/**
|
||||
* @internal
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Reset the fetcher for a given key
|
||||
* @param key
|
||||
*/
|
||||
resetFetcher(key: string, opts?: {
|
||||
reason?: unknown;
|
||||
}): void;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Delete the fetcher for a given key
|
||||
* @param key
|
||||
*/
|
||||
deleteFetcher(key: string): void;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Cleanup listeners and abort any in-progress loads
|
||||
*/
|
||||
dispose(): void;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Get a navigation blocker
|
||||
* @param key The identifier for the blocker
|
||||
* @param fn The blocker function implementation
|
||||
*/
|
||||
getBlocker(key: string, fn: BlockerFunction): Blocker;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Delete a navigation blocker
|
||||
* @param key The identifier for the blocker
|
||||
*/
|
||||
deleteBlocker(key: string): void;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE DO NOT USE
|
||||
*
|
||||
* Patch additional children routes into an existing parent route
|
||||
* @param routeId The parent route id or a callback function accepting `patch`
|
||||
* to perform batch patching
|
||||
* @param children The additional children routes
|
||||
* @param unstable_allowElementMutations Allow mutation or route elements on
|
||||
* existing routes. Intended for RSC-usage
|
||||
* only.
|
||||
*/
|
||||
patchRoutes(routeId: string | null, children: RouteObject[], unstable_allowElementMutations?: boolean): void;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* HMR needs to pass in-flight route updates to React Router
|
||||
* TODO: Replace this with granular route update APIs (addRoute, updateRoute, deleteRoute)
|
||||
*/
|
||||
_internalSetRoutes(routes: RouteObject[]): void;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Cause subscribers to re-render. This is used to force a re-render.
|
||||
*/
|
||||
_internalSetStateDoNotUseOrYouWillBreakYourApp(state: Partial<RouterState>): void;
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* Internal fetch AbortControllers accessed by unit tests
|
||||
*/
|
||||
_internalFetchControllers: Map<string, AbortController>;
|
||||
}
|
||||
/**
|
||||
* State maintained internally by the router. During a navigation, all states
|
||||
* reflect the "old" location unless otherwise noted.
|
||||
*/
|
||||
interface RouterState {
|
||||
/**
|
||||
* The action of the most recent navigation
|
||||
*/
|
||||
historyAction: Action;
|
||||
/**
|
||||
* The current location reflected by the router
|
||||
*/
|
||||
location: Location;
|
||||
/**
|
||||
* The current set of route matches
|
||||
*/
|
||||
matches: DataRouteMatch[];
|
||||
/**
|
||||
* Tracks whether we've completed our initial data load
|
||||
*/
|
||||
initialized: boolean;
|
||||
/**
|
||||
* Tracks whether we should be rendering a HydrateFallback during hydration
|
||||
*/
|
||||
renderFallback: boolean;
|
||||
/**
|
||||
* Current scroll position we should start at for a new view
|
||||
* - number -> scroll position to restore to
|
||||
* - false -> do not restore scroll at all (used during submissions/revalidations)
|
||||
* - null -> don't have a saved position, scroll to hash or top of page
|
||||
*/
|
||||
restoreScrollPosition: number | false | null;
|
||||
/**
|
||||
* Indicate whether this navigation should skip resetting the scroll position
|
||||
* if we are unable to restore the scroll position
|
||||
*/
|
||||
preventScrollReset: boolean;
|
||||
/**
|
||||
* Tracks the state of the current navigation
|
||||
*/
|
||||
navigation: Navigation;
|
||||
/**
|
||||
* Tracks any in-progress revalidations
|
||||
*/
|
||||
revalidation: RevalidationState;
|
||||
/**
|
||||
* Data from the loaders for the current matches
|
||||
*/
|
||||
loaderData: RouteData;
|
||||
/**
|
||||
* Data from the action for the current matches
|
||||
*/
|
||||
actionData: RouteData | null;
|
||||
/**
|
||||
* Errors caught from loaders for the current matches
|
||||
*/
|
||||
errors: RouteData | null;
|
||||
/**
|
||||
* Map of current fetchers
|
||||
*/
|
||||
fetchers: Map<string, Fetcher>;
|
||||
/**
|
||||
* Map of current blockers
|
||||
*/
|
||||
blockers: Map<string, Blocker>;
|
||||
}
|
||||
/**
|
||||
* Data that can be passed into hydrate a Router from SSR
|
||||
*/
|
||||
type HydrationState = Partial<Pick<RouterState, "loaderData" | "actionData" | "errors">>;
|
||||
/**
|
||||
* Future flags to toggle new feature behavior
|
||||
*/
|
||||
interface FutureConfig {
|
||||
}
|
||||
/**
|
||||
* Initialization options for createRouter
|
||||
*/
|
||||
interface RouterInit {
|
||||
routes: RouteObject[];
|
||||
history: History;
|
||||
basename?: string;
|
||||
getContext?: () => MaybePromise<RouterContextProvider>;
|
||||
instrumentations?: ClientInstrumentation[];
|
||||
mapRouteProperties?: MapRoutePropertiesFunction;
|
||||
future?: Partial<FutureConfig>;
|
||||
hydrationRouteProperties?: string[];
|
||||
hydrationData?: HydrationState;
|
||||
window?: Window;
|
||||
dataStrategy?: DataStrategyFunction;
|
||||
patchRoutesOnNavigation?: PatchRoutesOnNavigationFunction;
|
||||
}
|
||||
/**
|
||||
* State returned from a server-side query() call
|
||||
*/
|
||||
interface StaticHandlerContext {
|
||||
basename: Router["basename"];
|
||||
location: RouterState["location"];
|
||||
matches: RouterState["matches"];
|
||||
loaderData: RouterState["loaderData"];
|
||||
actionData: RouterState["actionData"];
|
||||
errors: RouterState["errors"];
|
||||
statusCode: number;
|
||||
loaderHeaders: Record<string, Headers>;
|
||||
actionHeaders: Record<string, Headers>;
|
||||
_deepestRenderedBoundaryId?: string | null;
|
||||
}
|
||||
/**
|
||||
* A StaticHandler instance manages a singular SSR navigation/fetch event
|
||||
*/
|
||||
interface StaticHandler {
|
||||
/**
|
||||
* The set of data routes managed by this handler
|
||||
*/
|
||||
dataRoutes: DataRouteObject[];
|
||||
/**
|
||||
* @private
|
||||
* PRIVATE - DO NOT USE
|
||||
*
|
||||
* The route branches derived from the data routes, used for internal route
|
||||
* matching in Framework Mode
|
||||
*/
|
||||
_internalRouteBranches: RouteBranch<DataRouteObject>[];
|
||||
/**
|
||||
* Perform a query for a given request - executing all matched route
|
||||
* loaders/actions. Used for document requests.
|
||||
*
|
||||
* @param request The request to query
|
||||
* @param opts Optional query options
|
||||
* @param opts.dataStrategy Alternate dataStrategy implementation
|
||||
* @param opts.filterMatchesToLoad Predicate function to filter which matches should be loaded
|
||||
* @param opts.generateMiddlewareResponse To enable middleware, provide a function
|
||||
* to generate a response to bubble back up the middleware chain
|
||||
* @param opts.requestContext Context object to pass to loaders/actions
|
||||
* @param opts.skipLoaderErrorBubbling Skip loader error bubbling
|
||||
* @param opts.skipRevalidation Skip revalidation after action submission
|
||||
* @param opts.normalizePath Normalize the request path
|
||||
*/
|
||||
query(request: Request, opts?: {
|
||||
requestContext?: unknown;
|
||||
filterMatchesToLoad?: (match: DataRouteMatch) => boolean;
|
||||
skipLoaderErrorBubbling?: boolean;
|
||||
skipRevalidation?: boolean;
|
||||
dataStrategy?: DataStrategyFunction<unknown>;
|
||||
generateMiddlewareResponse?: (query: (r: Request, args?: {
|
||||
filterMatchesToLoad?: (match: DataRouteMatch) => boolean;
|
||||
}) => Promise<StaticHandlerContext | Response>) => MaybePromise<Response>;
|
||||
normalizePath?: (request: Request) => Path;
|
||||
}): Promise<StaticHandlerContext | Response>;
|
||||
/**
|
||||
* Perform a query for a specific route. Used for resource requests.
|
||||
*
|
||||
* @param request The request to query
|
||||
* @param opts Optional queryRoute options
|
||||
* @param opts.dataStrategy Alternate dataStrategy implementation
|
||||
* @param opts.generateMiddlewareResponse To enable middleware, provide a function
|
||||
* to generate a response to bubble back up the middleware chain
|
||||
* @param opts.requestContext Context object to pass to loaders/actions
|
||||
* @param opts.routeId The ID of the route to query
|
||||
* @param opts.normalizePath Normalize the request path
|
||||
|
||||
*/
|
||||
queryRoute(request: Request, opts?: {
|
||||
routeId?: string;
|
||||
requestContext?: unknown;
|
||||
dataStrategy?: DataStrategyFunction<unknown>;
|
||||
generateMiddlewareResponse?: (queryRoute: (r: Request) => Promise<Response>) => MaybePromise<Response>;
|
||||
normalizePath?: (request: Request) => Path;
|
||||
}): Promise<any>;
|
||||
}
|
||||
type ViewTransitionOpts = {
|
||||
currentLocation: Location;
|
||||
nextLocation: Location;
|
||||
};
|
||||
/**
|
||||
* Subscriber function signature for changes to router state
|
||||
*/
|
||||
interface RouterSubscriber {
|
||||
(state: RouterState, opts: {
|
||||
deletedFetchers: string[];
|
||||
newErrors: RouteData | null;
|
||||
viewTransitionOpts?: ViewTransitionOpts;
|
||||
flushSync: boolean;
|
||||
}): void;
|
||||
}
|
||||
/**
|
||||
* Function signature for determining the key to be used in scroll restoration
|
||||
* for a given location
|
||||
*/
|
||||
interface GetScrollRestorationKeyFunction {
|
||||
(location: Location, matches: UIMatch[]): string | null;
|
||||
}
|
||||
/**
|
||||
* Function signature for determining the current scroll position
|
||||
*/
|
||||
interface GetScrollPositionFunction {
|
||||
(): number;
|
||||
}
|
||||
/**
|
||||
* - "route": relative to the route hierarchy so `..` means remove all segments
|
||||
* of the current route even if it has many. For example, a `route("posts/:id")`
|
||||
* would have both `:id` and `posts` removed from the url.
|
||||
* - "path": relative to the pathname so `..` means remove one segment of the
|
||||
* pathname. For example, a `route("posts/:id")` would have only `:id` removed
|
||||
* from the url.
|
||||
*/
|
||||
type RelativeRoutingType = "route" | "path";
|
||||
type BaseNavigateOrFetchOptions = {
|
||||
preventScrollReset?: boolean;
|
||||
relative?: RelativeRoutingType;
|
||||
flushSync?: boolean;
|
||||
defaultShouldRevalidate?: boolean;
|
||||
};
|
||||
type BaseNavigateOptions = BaseNavigateOrFetchOptions & {
|
||||
replace?: boolean;
|
||||
state?: any;
|
||||
fromRouteId?: string;
|
||||
viewTransition?: boolean;
|
||||
mask?: To;
|
||||
};
|
||||
type BaseSubmissionOptions = {
|
||||
formMethod?: HTMLFormMethod;
|
||||
formEncType?: FormEncType;
|
||||
} & ({
|
||||
formData: FormData;
|
||||
body?: undefined;
|
||||
} | {
|
||||
formData?: undefined;
|
||||
body: any;
|
||||
});
|
||||
/**
|
||||
* Options for a navigate() call for a normal (non-submission) navigation
|
||||
*/
|
||||
type LinkNavigateOptions = BaseNavigateOptions;
|
||||
/**
|
||||
* Options for a navigate() call for a submission navigation
|
||||
*/
|
||||
type SubmissionNavigateOptions = BaseNavigateOptions & BaseSubmissionOptions;
|
||||
/**
|
||||
* Options to pass to navigate() for a navigation
|
||||
*/
|
||||
type RouterNavigateOptions = LinkNavigateOptions | SubmissionNavigateOptions;
|
||||
/**
|
||||
* Options for a fetch() load
|
||||
*/
|
||||
type LoadFetchOptions = BaseNavigateOrFetchOptions;
|
||||
/**
|
||||
* Options for a fetch() submission
|
||||
*/
|
||||
type SubmitFetchOptions = BaseNavigateOrFetchOptions & BaseSubmissionOptions;
|
||||
/**
|
||||
* Options to pass to fetch()
|
||||
*/
|
||||
type RouterFetchOptions = LoadFetchOptions | SubmitFetchOptions;
|
||||
/**
|
||||
* Potential states for state.navigation
|
||||
*/
|
||||
type NavigationStates = {
|
||||
Idle: {
|
||||
state: "idle";
|
||||
location: undefined;
|
||||
matches: undefined;
|
||||
historyAction: undefined;
|
||||
formMethod: undefined;
|
||||
formAction: undefined;
|
||||
formEncType: undefined;
|
||||
formData: undefined;
|
||||
json: undefined;
|
||||
text: undefined;
|
||||
};
|
||||
Loading: {
|
||||
state: "loading";
|
||||
location: Location;
|
||||
matches: DataRouteMatch[];
|
||||
historyAction: Action;
|
||||
formMethod: Submission["formMethod"] | undefined;
|
||||
formAction: Submission["formAction"] | undefined;
|
||||
formEncType: Submission["formEncType"] | undefined;
|
||||
formData: Submission["formData"] | undefined;
|
||||
json: Submission["json"] | undefined;
|
||||
text: Submission["text"] | undefined;
|
||||
};
|
||||
Submitting: {
|
||||
state: "submitting";
|
||||
location: Location;
|
||||
matches: DataRouteMatch[];
|
||||
historyAction: Action;
|
||||
formMethod: Submission["formMethod"];
|
||||
formAction: Submission["formAction"];
|
||||
formEncType: Submission["formEncType"];
|
||||
formData: Submission["formData"];
|
||||
json: Submission["json"];
|
||||
text: Submission["text"];
|
||||
};
|
||||
};
|
||||
type Navigation = NavigationStates[keyof NavigationStates];
|
||||
type RevalidationState = "idle" | "loading";
|
||||
/**
|
||||
* Potential states for fetchers
|
||||
*/
|
||||
type FetcherStates<TData = any> = {
|
||||
/**
|
||||
* The fetcher is not calling a loader or action
|
||||
*
|
||||
* ```tsx
|
||||
* fetcher.state === "idle"
|
||||
* ```
|
||||
*/
|
||||
Idle: {
|
||||
state: "idle";
|
||||
formMethod: undefined;
|
||||
formAction: undefined;
|
||||
formEncType: undefined;
|
||||
text: undefined;
|
||||
formData: undefined;
|
||||
json: undefined;
|
||||
/**
|
||||
* If the fetcher has never been called, this will be undefined.
|
||||
*/
|
||||
data: TData | undefined;
|
||||
};
|
||||
/**
|
||||
* The fetcher is loading data from a {@link LoaderFunction | loader} from a
|
||||
* call to {@link FetcherWithComponents.load | `fetcher.load`}.
|
||||
*
|
||||
* ```tsx
|
||||
* // somewhere
|
||||
* <button onClick={() => fetcher.load("/some/route") }>Load</button>
|
||||
*
|
||||
* // the state will update
|
||||
* fetcher.state === "loading"
|
||||
* ```
|
||||
*/
|
||||
Loading: {
|
||||
state: "loading";
|
||||
formMethod: Submission["formMethod"] | undefined;
|
||||
formAction: Submission["formAction"] | undefined;
|
||||
formEncType: Submission["formEncType"] | undefined;
|
||||
text: Submission["text"] | undefined;
|
||||
formData: Submission["formData"] | undefined;
|
||||
json: Submission["json"] | undefined;
|
||||
data: TData | undefined;
|
||||
};
|
||||
/**
|
||||
The fetcher is submitting to a {@link LoaderFunction} (GET) or {@link ActionFunction} (POST) from a {@link FetcherWithComponents.Form | `fetcher.Form`} or {@link FetcherWithComponents.submit | `fetcher.submit`}.
|
||||
|
||||
```tsx
|
||||
// somewhere
|
||||
<input
|
||||
onChange={e => {
|
||||
fetcher.submit(event.currentTarget.form, { method: "post" });
|
||||
}}
|
||||
/>
|
||||
|
||||
// the state will update
|
||||
fetcher.state === "submitting"
|
||||
|
||||
// and formData will be available
|
||||
fetcher.formData
|
||||
```
|
||||
*/
|
||||
Submitting: {
|
||||
state: "submitting";
|
||||
formMethod: Submission["formMethod"];
|
||||
formAction: Submission["formAction"];
|
||||
formEncType: Submission["formEncType"];
|
||||
text: Submission["text"];
|
||||
formData: Submission["formData"];
|
||||
json: Submission["json"];
|
||||
data: TData | undefined;
|
||||
};
|
||||
};
|
||||
type Fetcher<TData = any> = FetcherStates<TData>[keyof FetcherStates<TData>];
|
||||
interface BlockerBlocked {
|
||||
state: "blocked";
|
||||
reset: () => void;
|
||||
proceed: () => void;
|
||||
location: Location;
|
||||
}
|
||||
interface BlockerUnblocked {
|
||||
state: "unblocked";
|
||||
reset: undefined;
|
||||
proceed: undefined;
|
||||
location: undefined;
|
||||
}
|
||||
interface BlockerProceeding {
|
||||
state: "proceeding";
|
||||
reset: undefined;
|
||||
proceed: undefined;
|
||||
location: Location;
|
||||
}
|
||||
type Blocker = BlockerUnblocked | BlockerBlocked | BlockerProceeding;
|
||||
type BlockerFunction = (args: {
|
||||
currentLocation: Location;
|
||||
nextLocation: Location;
|
||||
historyAction: Action;
|
||||
}) => boolean;
|
||||
declare const IDLE_NAVIGATION: NavigationStates["Idle"];
|
||||
declare const IDLE_FETCHER: FetcherStates["Idle"];
|
||||
declare const IDLE_BLOCKER: BlockerUnblocked;
|
||||
/**
|
||||
* Create a router and listen to history POP navigations
|
||||
*/
|
||||
declare function createRouter(init: RouterInit): Router;
|
||||
interface CreateStaticHandlerOptions {
|
||||
basename?: string;
|
||||
mapRouteProperties?: MapRoutePropertiesFunction;
|
||||
instrumentations?: Pick<ServerInstrumentation, "route">[];
|
||||
future?: Partial<FutureConfig>;
|
||||
}
|
||||
|
||||
type ServerInstrumentation = {
|
||||
handler?: InstrumentRequestHandlerFunction;
|
||||
route?: InstrumentRouteFunction;
|
||||
};
|
||||
type ClientInstrumentation = {
|
||||
router?: InstrumentRouterFunction;
|
||||
route?: InstrumentRouteFunction;
|
||||
};
|
||||
type InstrumentRequestHandlerFunction = (handler: InstrumentableRequestHandler) => void;
|
||||
type InstrumentRouterFunction = (router: InstrumentableRouter) => void;
|
||||
type InstrumentRouteFunction = (route: InstrumentableRoute) => void;
|
||||
type InstrumentationHandlerResult = {
|
||||
status: "success";
|
||||
error: undefined;
|
||||
} | {
|
||||
status: "error";
|
||||
error: Error;
|
||||
};
|
||||
type InstrumentFunction<T> = (handler: () => Promise<InstrumentationHandlerResult>, info: T) => Promise<void>;
|
||||
type ReadonlyRequest = {
|
||||
method: string;
|
||||
url: string;
|
||||
headers: Pick<Headers, "get">;
|
||||
};
|
||||
type ReadonlyContext = MiddlewareEnabled extends true ? Pick<RouterContextProvider, "get"> : Readonly<AppLoadContext>;
|
||||
type InstrumentableRoute = {
|
||||
id: string;
|
||||
index: boolean | undefined;
|
||||
path: string | undefined;
|
||||
instrument(instrumentations: RouteInstrumentations): void;
|
||||
};
|
||||
type RouteInstrumentations = {
|
||||
lazy?: InstrumentFunction<RouteLazyInstrumentationInfo>;
|
||||
"lazy.loader"?: InstrumentFunction<RouteLazyInstrumentationInfo>;
|
||||
"lazy.action"?: InstrumentFunction<RouteLazyInstrumentationInfo>;
|
||||
"lazy.middleware"?: InstrumentFunction<RouteLazyInstrumentationInfo>;
|
||||
middleware?: InstrumentFunction<RouteHandlerInstrumentationInfo>;
|
||||
loader?: InstrumentFunction<RouteHandlerInstrumentationInfo>;
|
||||
action?: InstrumentFunction<RouteHandlerInstrumentationInfo>;
|
||||
};
|
||||
type RouteLazyInstrumentationInfo = undefined;
|
||||
type RouteHandlerInstrumentationInfo = Readonly<{
|
||||
request: ReadonlyRequest;
|
||||
params: LoaderFunctionArgs["params"];
|
||||
pattern: string;
|
||||
context: ReadonlyContext;
|
||||
}>;
|
||||
type InstrumentableRouter = {
|
||||
instrument(instrumentations: RouterInstrumentations): void;
|
||||
};
|
||||
type RouterInstrumentations = {
|
||||
navigate?: InstrumentFunction<RouterNavigationInstrumentationInfo>;
|
||||
fetch?: InstrumentFunction<RouterFetchInstrumentationInfo>;
|
||||
};
|
||||
type RouterNavigationInstrumentationInfo = Readonly<{
|
||||
to: string | number;
|
||||
currentUrl: string;
|
||||
formMethod?: HTMLFormMethod;
|
||||
formEncType?: FormEncType;
|
||||
formData?: FormData;
|
||||
body?: any;
|
||||
}>;
|
||||
type RouterFetchInstrumentationInfo = Readonly<{
|
||||
href: string;
|
||||
currentUrl: string;
|
||||
fetcherKey: string;
|
||||
formMethod?: HTMLFormMethod;
|
||||
formEncType?: FormEncType;
|
||||
formData?: FormData;
|
||||
body?: any;
|
||||
}>;
|
||||
type InstrumentableRequestHandler = {
|
||||
instrument(instrumentations: RequestHandlerInstrumentations): void;
|
||||
};
|
||||
type RequestHandlerInstrumentations = {
|
||||
request?: InstrumentFunction<RequestHandlerInstrumentationInfo>;
|
||||
};
|
||||
type RequestHandlerInstrumentationInfo = Readonly<{
|
||||
request: ReadonlyRequest;
|
||||
context: ReadonlyContext | undefined;
|
||||
}>;
|
||||
|
||||
export { type BlockerFunction as B, type ClientInstrumentation as C, type Fetcher as F, type GetScrollPositionFunction as G, type HydrationState as H, type InstrumentRequestHandlerFunction as I, type NavigationStates as N, type RouterInit as R, type StaticHandler as S, type Router as a, type Blocker as b, type RelativeRoutingType as c, type GetScrollRestorationKeyFunction as d, type StaticHandlerContext as e, type Navigation as f, type RouterState as g, type RouterSubscriber as h, type RouterNavigateOptions as i, type RouterFetchOptions as j, type RevalidationState as k, type ServerInstrumentation as l, type InstrumentRouterFunction as m, type InstrumentRouteFunction as n, type InstrumentationHandlerResult as o, IDLE_NAVIGATION as p, IDLE_FETCHER as q, IDLE_BLOCKER as r, createRouter as s, type FutureConfig as t, type CreateStaticHandlerOptions as u };
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
import { R as RouteModule, e as LinkDescriptor, L as Location, F as Func, f as Pretty, g as MetaDescriptor, G as GetLoaderData, h as ServerDataFunctionArgs, i as MiddlewareNextFunction, j as ClientDataFunctionArgs, D as DataStrategyResult, k as ServerDataFrom, N as Normalize, l as GetActionData } from '../../data-DEjBmEfD.mjs';
|
||||
import { R as RouteFiles, P as Pages } from '../../register-CmkRspdl.mjs';
|
||||
import 'react';
|
||||
|
||||
type MaybePromise<T> = T | Promise<T>;
|
||||
type Props = {
|
||||
params: unknown;
|
||||
loaderData: unknown;
|
||||
actionData: unknown;
|
||||
};
|
||||
type RouteInfo = Props & {
|
||||
module: RouteModule;
|
||||
matches: Array<MatchInfo>;
|
||||
};
|
||||
type MatchInfo = {
|
||||
id: string;
|
||||
module: RouteModule;
|
||||
};
|
||||
type MetaMatch<T extends MatchInfo> = Pretty<{
|
||||
id: T["id"];
|
||||
params: Record<string, string | undefined>;
|
||||
pathname: string;
|
||||
meta: MetaDescriptor[];
|
||||
/** @deprecated Use `MetaMatch.loaderData` instead */
|
||||
data: GetLoaderData<T["module"]>;
|
||||
loaderData: GetLoaderData<T["module"]>;
|
||||
handle?: unknown;
|
||||
error?: unknown;
|
||||
}>;
|
||||
type MetaMatches<T extends Array<MatchInfo>> = T extends [infer F extends MatchInfo, ...infer R extends Array<MatchInfo>] ? [MetaMatch<F>, ...MetaMatches<R>] : Array<MetaMatch<MatchInfo> | undefined>;
|
||||
type HasErrorBoundary<T extends RouteInfo> = T["module"] extends {
|
||||
ErrorBoundary: Func;
|
||||
} ? true : false;
|
||||
type CreateMetaArgs<T extends RouteInfo> = {
|
||||
/** This is the current router `Location` object. This is useful for generating tags for routes at specific paths or query parameters. */
|
||||
location: Location;
|
||||
/** {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route. */
|
||||
params: T["params"];
|
||||
/**
|
||||
* The return value for this route's server loader function
|
||||
*
|
||||
* @deprecated Use `Route.MetaArgs.loaderData` instead
|
||||
*/
|
||||
data: T["loaderData"] | (HasErrorBoundary<T> extends true ? undefined : never);
|
||||
/** The return value for this route's server loader function */
|
||||
loaderData: T["loaderData"] | (HasErrorBoundary<T> extends true ? undefined : never);
|
||||
/** Thrown errors that trigger error boundaries will be passed to the meta function. This is useful for generating metadata for error pages. */
|
||||
error?: unknown;
|
||||
/** An array of the current {@link https://api.reactrouter.com/v7/interfaces/react-router.UIMatch.html route matches}, including parent route matches. */
|
||||
matches: MetaMatches<T["matches"]>;
|
||||
};
|
||||
type MetaDescriptors = MetaDescriptor[];
|
||||
type HeadersArgs = {
|
||||
loaderHeaders: Headers;
|
||||
parentHeaders: Headers;
|
||||
actionHeaders: Headers;
|
||||
errorHeaders: Headers | undefined;
|
||||
};
|
||||
type CreateServerMiddlewareFunction<T extends RouteInfo> = (args: ServerDataFunctionArgs<T["params"]>, next: MiddlewareNextFunction<Response>) => MaybePromise<Response | void>;
|
||||
type CreateClientMiddlewareFunction<T extends RouteInfo> = (args: ClientDataFunctionArgs<T["params"]>, next: MiddlewareNextFunction<Record<string, DataStrategyResult>>) => MaybePromise<Record<string, DataStrategyResult> | void>;
|
||||
type CreateServerLoaderArgs<T extends RouteInfo> = ServerDataFunctionArgs<T["params"]>;
|
||||
type CreateClientLoaderArgs<T extends RouteInfo> = ClientDataFunctionArgs<T["params"]> & {
|
||||
/** This is an asynchronous function to get the data from the server loader for this route. On client-side navigations, this will make a {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API fetch} call to the React Router server loader. If you opt-into running your clientLoader on hydration, then this function will return the data that was already loaded on the server (via Promise.resolve). */
|
||||
serverLoader: () => Promise<ServerDataFrom<T["module"]["loader"]>>;
|
||||
};
|
||||
type CreateServerActionArgs<T extends RouteInfo> = ServerDataFunctionArgs<T["params"]>;
|
||||
type CreateClientActionArgs<T extends RouteInfo> = ClientDataFunctionArgs<T["params"]> & {
|
||||
/** This is an asynchronous function that makes the {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API fetch} call to the React Router server action for this route. */
|
||||
serverAction: () => Promise<ServerDataFrom<T["module"]["action"]>>;
|
||||
};
|
||||
type CreateHydrateFallbackProps<T extends RouteInfo, RSCEnabled extends boolean> = {
|
||||
params: T["params"];
|
||||
} & (RSCEnabled extends true ? {
|
||||
/** The data returned from the `loader` */
|
||||
loaderData?: ServerDataFrom<T["module"]["loader"]>;
|
||||
/** The data returned from the `action` following an action submission. */
|
||||
actionData?: ServerDataFrom<T["module"]["action"]>;
|
||||
} : {
|
||||
/** The data returned from the `loader` or `clientLoader` */
|
||||
loaderData?: T["loaderData"];
|
||||
/** The data returned from the `action` or `clientAction` following an action submission. */
|
||||
actionData?: T["actionData"];
|
||||
});
|
||||
type Match<T extends MatchInfo> = Pretty<{
|
||||
id: T["id"];
|
||||
params: Record<string, string | undefined>;
|
||||
pathname: string;
|
||||
/** @deprecated Use `Match.loaderData` instead */
|
||||
data: GetLoaderData<T["module"]>;
|
||||
loaderData: GetLoaderData<T["module"]>;
|
||||
handle: unknown;
|
||||
}>;
|
||||
type Matches<T extends Array<MatchInfo>> = T extends [infer F extends MatchInfo, ...infer R extends Array<MatchInfo>] ? [Match<F>, ...Matches<R>] : Array<Match<MatchInfo> | undefined>;
|
||||
type CreateComponentProps<T extends RouteInfo, RSCEnabled extends boolean> = {
|
||||
/**
|
||||
* {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route.
|
||||
* @example
|
||||
* // app/routes.ts
|
||||
* route("teams/:teamId", "./team.tsx"),
|
||||
*
|
||||
* // app/team.tsx
|
||||
* export default function Component({
|
||||
* params,
|
||||
* }: Route.ComponentProps) {
|
||||
* params.teamId;
|
||||
* // ^ string
|
||||
* }
|
||||
**/
|
||||
params: T["params"];
|
||||
/** An array of the current {@link https://api.reactrouter.com/v7/interfaces/react-router.UIMatch.html route matches}, including parent route matches. */
|
||||
matches: Matches<T["matches"]>;
|
||||
} & (RSCEnabled extends true ? {
|
||||
/** The data returned from the `loader` */
|
||||
loaderData: ServerDataFrom<T["module"]["loader"]>;
|
||||
/** The data returned from the `action` following an action submission. */
|
||||
actionData?: ServerDataFrom<T["module"]["action"]>;
|
||||
} : {
|
||||
/** The data returned from the `loader` or `clientLoader` */
|
||||
loaderData: T["loaderData"];
|
||||
/** The data returned from the `action` or `clientAction` following an action submission. */
|
||||
actionData?: T["actionData"];
|
||||
});
|
||||
type CreateErrorBoundaryProps<T extends RouteInfo, RSCEnabled extends boolean> = {
|
||||
/**
|
||||
* {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route.
|
||||
* @example
|
||||
* // app/routes.ts
|
||||
* route("teams/:teamId", "./team.tsx"),
|
||||
*
|
||||
* // app/team.tsx
|
||||
* export function ErrorBoundary({
|
||||
* params,
|
||||
* }: Route.ErrorBoundaryProps) {
|
||||
* params.teamId;
|
||||
* // ^ string
|
||||
* }
|
||||
**/
|
||||
params: T["params"];
|
||||
error: unknown;
|
||||
} & (RSCEnabled extends true ? {
|
||||
/** The data returned from the `loader` */
|
||||
loaderData?: ServerDataFrom<T["module"]["loader"]>;
|
||||
/** The data returned from the `action` following an action submission. */
|
||||
actionData?: ServerDataFrom<T["module"]["action"]>;
|
||||
} : {
|
||||
/** The data returned from the `loader` or `clientLoader` */
|
||||
loaderData?: T["loaderData"];
|
||||
/** The data returned from the `action` or `clientAction` following an action submission. */
|
||||
actionData?: T["actionData"];
|
||||
});
|
||||
type GetAnnotations<Info extends RouteInfo> = {
|
||||
LinkDescriptors: LinkDescriptor[];
|
||||
LinksFunction: () => LinkDescriptor[];
|
||||
MetaArgs: CreateMetaArgs<Info>;
|
||||
MetaDescriptors: MetaDescriptors;
|
||||
MetaFunction: (args: CreateMetaArgs<Info>) => MetaDescriptors;
|
||||
HeadersArgs: HeadersArgs;
|
||||
HeadersFunction: (args: HeadersArgs) => Headers | HeadersInit;
|
||||
MiddlewareFunction: CreateServerMiddlewareFunction<Info>;
|
||||
ClientMiddlewareFunction: CreateClientMiddlewareFunction<Info>;
|
||||
LoaderArgs: CreateServerLoaderArgs<Info>;
|
||||
ClientLoaderArgs: CreateClientLoaderArgs<Info>;
|
||||
ActionArgs: CreateServerActionArgs<Info>;
|
||||
ClientActionArgs: CreateClientActionArgs<Info>;
|
||||
HydrateFallbackProps: CreateHydrateFallbackProps<Info, false>;
|
||||
ServerHydrateFallbackProps: CreateHydrateFallbackProps<Info, true>;
|
||||
ComponentProps: CreateComponentProps<Info, false>;
|
||||
ServerComponentProps: CreateComponentProps<Info, true>;
|
||||
ErrorBoundaryProps: CreateErrorBoundaryProps<Info, false>;
|
||||
ServerErrorBoundaryProps: CreateErrorBoundaryProps<Info, true>;
|
||||
};
|
||||
|
||||
type Params<RouteFile extends keyof RouteFiles> = Normalize<Pages[RouteFiles[RouteFile]["page"]]["params"]>;
|
||||
|
||||
type GetInfo<T extends {
|
||||
file: keyof RouteFiles;
|
||||
module: RouteModule;
|
||||
}> = {
|
||||
params: Params<T["file"]>;
|
||||
loaderData: GetLoaderData<T["module"]>;
|
||||
actionData: GetActionData<T["module"]>;
|
||||
};
|
||||
|
||||
export type { GetAnnotations, GetInfo };
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
import { R as RouteModule, v as LinkDescriptor, L as Location, w as Func, x as Pretty, y as MetaDescriptor, G as GetLoaderData, z as ServerDataFunctionArgs, B as MiddlewareNextFunction, E as ClientDataFunctionArgs, I as DataStrategyResult, J as ServerDataFrom, N as Normalize, K as GetActionData } from '../../data-CjO11-hU.js';
|
||||
import { R as RouteFiles, P as Pages } from '../../register-roq_0qYo.js';
|
||||
import 'react';
|
||||
|
||||
type MaybePromise<T> = T | Promise<T>;
|
||||
type Props = {
|
||||
params: unknown;
|
||||
loaderData: unknown;
|
||||
actionData: unknown;
|
||||
};
|
||||
type RouteInfo = Props & {
|
||||
module: RouteModule;
|
||||
matches: Array<MatchInfo>;
|
||||
};
|
||||
type MatchInfo = {
|
||||
id: string;
|
||||
module: RouteModule;
|
||||
};
|
||||
type MetaMatch<T extends MatchInfo> = Pretty<{
|
||||
id: T["id"];
|
||||
params: Record<string, string | undefined>;
|
||||
pathname: string;
|
||||
meta: MetaDescriptor[];
|
||||
/** @deprecated Use `MetaMatch.loaderData` instead */
|
||||
data: GetLoaderData<T["module"]>;
|
||||
loaderData: GetLoaderData<T["module"]>;
|
||||
handle?: unknown;
|
||||
error?: unknown;
|
||||
}>;
|
||||
type MetaMatches<T extends Array<MatchInfo>> = T extends [infer F extends MatchInfo, ...infer R extends Array<MatchInfo>] ? [MetaMatch<F>, ...MetaMatches<R>] : Array<MetaMatch<MatchInfo> | undefined>;
|
||||
type HasErrorBoundary<T extends RouteInfo> = T["module"] extends {
|
||||
ErrorBoundary: Func;
|
||||
} ? true : false;
|
||||
type CreateMetaArgs<T extends RouteInfo> = {
|
||||
/** This is the current router `Location` object. This is useful for generating tags for routes at specific paths or query parameters. */
|
||||
location: Location;
|
||||
/** {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route. */
|
||||
params: T["params"];
|
||||
/**
|
||||
* The return value for this route's server loader function
|
||||
*
|
||||
* @deprecated Use `Route.MetaArgs.loaderData` instead
|
||||
*/
|
||||
data: T["loaderData"] | (HasErrorBoundary<T> extends true ? undefined : never);
|
||||
/** The return value for this route's server loader function */
|
||||
loaderData: T["loaderData"] | (HasErrorBoundary<T> extends true ? undefined : never);
|
||||
/** Thrown errors that trigger error boundaries will be passed to the meta function. This is useful for generating metadata for error pages. */
|
||||
error?: unknown;
|
||||
/** An array of the current {@link https://api.reactrouter.com/v7/interfaces/react-router.UIMatch.html route matches}, including parent route matches. */
|
||||
matches: MetaMatches<T["matches"]>;
|
||||
};
|
||||
type MetaDescriptors = MetaDescriptor[];
|
||||
type HeadersArgs = {
|
||||
loaderHeaders: Headers;
|
||||
parentHeaders: Headers;
|
||||
actionHeaders: Headers;
|
||||
errorHeaders: Headers | undefined;
|
||||
};
|
||||
type CreateServerMiddlewareFunction<T extends RouteInfo> = (args: ServerDataFunctionArgs<T["params"]>, next: MiddlewareNextFunction<Response>) => MaybePromise<Response | void>;
|
||||
type CreateClientMiddlewareFunction<T extends RouteInfo> = (args: ClientDataFunctionArgs<T["params"]>, next: MiddlewareNextFunction<Record<string, DataStrategyResult>>) => MaybePromise<Record<string, DataStrategyResult> | void>;
|
||||
type CreateServerLoaderArgs<T extends RouteInfo> = ServerDataFunctionArgs<T["params"]>;
|
||||
type CreateClientLoaderArgs<T extends RouteInfo> = ClientDataFunctionArgs<T["params"]> & {
|
||||
/** This is an asynchronous function to get the data from the server loader for this route. On client-side navigations, this will make a {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API fetch} call to the React Router server loader. If you opt-into running your clientLoader on hydration, then this function will return the data that was already loaded on the server (via Promise.resolve). */
|
||||
serverLoader: () => Promise<ServerDataFrom<T["module"]["loader"]>>;
|
||||
};
|
||||
type CreateServerActionArgs<T extends RouteInfo> = ServerDataFunctionArgs<T["params"]>;
|
||||
type CreateClientActionArgs<T extends RouteInfo> = ClientDataFunctionArgs<T["params"]> & {
|
||||
/** This is an asynchronous function that makes the {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API fetch} call to the React Router server action for this route. */
|
||||
serverAction: () => Promise<ServerDataFrom<T["module"]["action"]>>;
|
||||
};
|
||||
type CreateHydrateFallbackProps<T extends RouteInfo, RSCEnabled extends boolean> = {
|
||||
params: T["params"];
|
||||
} & (RSCEnabled extends true ? {
|
||||
/** The data returned from the `loader` */
|
||||
loaderData?: ServerDataFrom<T["module"]["loader"]>;
|
||||
/** The data returned from the `action` following an action submission. */
|
||||
actionData?: ServerDataFrom<T["module"]["action"]>;
|
||||
} : {
|
||||
/** The data returned from the `loader` or `clientLoader` */
|
||||
loaderData?: T["loaderData"];
|
||||
/** The data returned from the `action` or `clientAction` following an action submission. */
|
||||
actionData?: T["actionData"];
|
||||
});
|
||||
type Match<T extends MatchInfo> = Pretty<{
|
||||
id: T["id"];
|
||||
params: Record<string, string | undefined>;
|
||||
pathname: string;
|
||||
/** @deprecated Use `Match.loaderData` instead */
|
||||
data: GetLoaderData<T["module"]>;
|
||||
loaderData: GetLoaderData<T["module"]>;
|
||||
handle: unknown;
|
||||
}>;
|
||||
type Matches<T extends Array<MatchInfo>> = T extends [infer F extends MatchInfo, ...infer R extends Array<MatchInfo>] ? [Match<F>, ...Matches<R>] : Array<Match<MatchInfo> | undefined>;
|
||||
type CreateComponentProps<T extends RouteInfo, RSCEnabled extends boolean> = {
|
||||
/**
|
||||
* {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route.
|
||||
* @example
|
||||
* // app/routes.ts
|
||||
* route("teams/:teamId", "./team.tsx"),
|
||||
*
|
||||
* // app/team.tsx
|
||||
* export default function Component({
|
||||
* params,
|
||||
* }: Route.ComponentProps) {
|
||||
* params.teamId;
|
||||
* // ^ string
|
||||
* }
|
||||
**/
|
||||
params: T["params"];
|
||||
/** An array of the current {@link https://api.reactrouter.com/v7/interfaces/react-router.UIMatch.html route matches}, including parent route matches. */
|
||||
matches: Matches<T["matches"]>;
|
||||
} & (RSCEnabled extends true ? {
|
||||
/** The data returned from the `loader` */
|
||||
loaderData: ServerDataFrom<T["module"]["loader"]>;
|
||||
/** The data returned from the `action` following an action submission. */
|
||||
actionData?: ServerDataFrom<T["module"]["action"]>;
|
||||
} : {
|
||||
/** The data returned from the `loader` or `clientLoader` */
|
||||
loaderData: T["loaderData"];
|
||||
/** The data returned from the `action` or `clientAction` following an action submission. */
|
||||
actionData?: T["actionData"];
|
||||
});
|
||||
type CreateErrorBoundaryProps<T extends RouteInfo, RSCEnabled extends boolean> = {
|
||||
/**
|
||||
* {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route.
|
||||
* @example
|
||||
* // app/routes.ts
|
||||
* route("teams/:teamId", "./team.tsx"),
|
||||
*
|
||||
* // app/team.tsx
|
||||
* export function ErrorBoundary({
|
||||
* params,
|
||||
* }: Route.ErrorBoundaryProps) {
|
||||
* params.teamId;
|
||||
* // ^ string
|
||||
* }
|
||||
**/
|
||||
params: T["params"];
|
||||
error: unknown;
|
||||
} & (RSCEnabled extends true ? {
|
||||
/** The data returned from the `loader` */
|
||||
loaderData?: ServerDataFrom<T["module"]["loader"]>;
|
||||
/** The data returned from the `action` following an action submission. */
|
||||
actionData?: ServerDataFrom<T["module"]["action"]>;
|
||||
} : {
|
||||
/** The data returned from the `loader` or `clientLoader` */
|
||||
loaderData?: T["loaderData"];
|
||||
/** The data returned from the `action` or `clientAction` following an action submission. */
|
||||
actionData?: T["actionData"];
|
||||
});
|
||||
type GetAnnotations<Info extends RouteInfo> = {
|
||||
LinkDescriptors: LinkDescriptor[];
|
||||
LinksFunction: () => LinkDescriptor[];
|
||||
MetaArgs: CreateMetaArgs<Info>;
|
||||
MetaDescriptors: MetaDescriptors;
|
||||
MetaFunction: (args: CreateMetaArgs<Info>) => MetaDescriptors;
|
||||
HeadersArgs: HeadersArgs;
|
||||
HeadersFunction: (args: HeadersArgs) => Headers | HeadersInit;
|
||||
MiddlewareFunction: CreateServerMiddlewareFunction<Info>;
|
||||
ClientMiddlewareFunction: CreateClientMiddlewareFunction<Info>;
|
||||
LoaderArgs: CreateServerLoaderArgs<Info>;
|
||||
ClientLoaderArgs: CreateClientLoaderArgs<Info>;
|
||||
ActionArgs: CreateServerActionArgs<Info>;
|
||||
ClientActionArgs: CreateClientActionArgs<Info>;
|
||||
HydrateFallbackProps: CreateHydrateFallbackProps<Info, false>;
|
||||
ServerHydrateFallbackProps: CreateHydrateFallbackProps<Info, true>;
|
||||
ComponentProps: CreateComponentProps<Info, false>;
|
||||
ServerComponentProps: CreateComponentProps<Info, true>;
|
||||
ErrorBoundaryProps: CreateErrorBoundaryProps<Info, false>;
|
||||
ServerErrorBoundaryProps: CreateErrorBoundaryProps<Info, true>;
|
||||
};
|
||||
|
||||
type Params<RouteFile extends keyof RouteFiles> = Normalize<Pages[RouteFiles[RouteFile]["page"]]["params"]>;
|
||||
|
||||
type GetInfo<T extends {
|
||||
file: keyof RouteFiles;
|
||||
module: RouteModule;
|
||||
}> = {
|
||||
params: Params<T["file"]>;
|
||||
loaderData: GetLoaderData<T["module"]>;
|
||||
actionData: GetActionData<T["module"]>;
|
||||
};
|
||||
|
||||
export type { GetAnnotations, GetInfo };
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
"use strict";/**
|
||||
* react-router v7.18.0
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* react-router v7.18.0
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { R as RouteModule } from './data-DEjBmEfD.mjs';
|
||||
|
||||
/**
|
||||
* Apps can use this interface to "register" app-wide types for React Router via interface declaration merging and module augmentation.
|
||||
* React Router should handle this for you via type generation.
|
||||
*
|
||||
* For more on declaration merging and module augmentation, see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation .
|
||||
*/
|
||||
interface Register {
|
||||
}
|
||||
type AnyParams = Record<string, string | undefined>;
|
||||
type AnyPages = Record<string, {
|
||||
params: AnyParams;
|
||||
}>;
|
||||
type Pages = Register extends {
|
||||
pages: infer Registered extends AnyPages;
|
||||
} ? Registered : AnyPages;
|
||||
type AnyRouteFiles = Record<string, {
|
||||
id: string;
|
||||
page: string;
|
||||
}>;
|
||||
type RouteFiles = Register extends {
|
||||
routeFiles: infer Registered extends AnyRouteFiles;
|
||||
} ? Registered : AnyRouteFiles;
|
||||
type AnyRouteModules = Record<string, RouteModule>;
|
||||
type RouteModules = Register extends {
|
||||
routeModules: infer Registered extends AnyRouteModules;
|
||||
} ? Registered : AnyRouteModules;
|
||||
|
||||
export type { Pages as P, RouteFiles as R, RouteModules as a, Register as b };
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { R as RouteModule } from './data-CjO11-hU.js';
|
||||
|
||||
/**
|
||||
* Apps can use this interface to "register" app-wide types for React Router via interface declaration merging and module augmentation.
|
||||
* React Router should handle this for you via type generation.
|
||||
*
|
||||
* For more on declaration merging and module augmentation, see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation .
|
||||
*/
|
||||
interface Register {
|
||||
}
|
||||
type AnyParams = Record<string, string | undefined>;
|
||||
type AnyPages = Record<string, {
|
||||
params: AnyParams;
|
||||
}>;
|
||||
type Pages = Register extends {
|
||||
pages: infer Registered extends AnyPages;
|
||||
} ? Registered : AnyPages;
|
||||
type AnyRouteFiles = Record<string, {
|
||||
id: string;
|
||||
page: string;
|
||||
}>;
|
||||
type RouteFiles = Register extends {
|
||||
routeFiles: infer Registered extends AnyRouteFiles;
|
||||
} ? Registered : AnyRouteFiles;
|
||||
type AnyRouteModules = Record<string, RouteModule>;
|
||||
type RouteModules = Register extends {
|
||||
routeModules: infer Registered extends AnyRouteModules;
|
||||
} ? Registered : AnyRouteModules;
|
||||
|
||||
export type { Pages as P, RouteFiles as R, RouteModules as a, Register as b };
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: Backend For Frontend
|
||||
---
|
||||
|
||||
# Backend For Frontend
|
||||
|
||||
[MODES: framework]
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
While React Router can serve as your fullstack application, it also fits perfectly into the "Backend for Frontend" architecture.
|
||||
|
||||
The BFF strategy employs a web server with a job scoped to serving the frontend web app and connecting it to the services it needs: your database, mailer, job queues, existing backend APIs (REST, GraphQL), etc. Instead of your UI integrating directly from the browser to these services, it connects to the BFF, and the BFF connects to your services.
|
||||
|
||||
Mature apps already have a lot of backend application code in Ruby, Elixir, PHP, etc., and there's no reason to justify migrating it all to a server-side JavaScript runtime just to get the benefits of React Router. Instead, you can use your React Router app as a backend for your frontend.
|
||||
|
||||
You can use `fetch` right from your loaders and actions to your backend.
|
||||
|
||||
```tsx lines=[7,13,17]
|
||||
import escapeHtml from "escape-html";
|
||||
|
||||
export async function loader() {
|
||||
const apiUrl = "https://api.example.com/some-data.json";
|
||||
const res = await fetch(apiUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.API_TOKEN}`,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
const prunedData = data.map((record) => {
|
||||
return {
|
||||
id: record.id,
|
||||
title: record.title,
|
||||
formattedBody: escapeHtml(record.content),
|
||||
};
|
||||
});
|
||||
return { prunedData };
|
||||
}
|
||||
```
|
||||
|
||||
There are several benefits of this approach vs. fetching directly from the browser. The highlighted lines above show how you can:
|
||||
|
||||
1. Simplify third-party integrations and keep tokens and secrets out of client bundles
|
||||
2. Prune the data down to send less kB over the network, speeding up your app significantly
|
||||
3. Move a lot of code from browser bundles to the server, like `escapeHtml`, which speeds up your app. Additionally, moving code to the server usually makes your code easier to maintain since server-side code doesn't have to worry about UI states for async operations
|
||||
|
||||
Again, React Router can be used as your only server by talking directly to the database and other services with server-side JavaScript APIs, but it also works perfectly as a backend for your frontend. Go ahead and keep your existing API server for application logic and let React Router connect the UI to it.
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: Automatic Code Splitting
|
||||
---
|
||||
|
||||
# Automatic Code Splitting
|
||||
|
||||
[MODES: framework]
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
When using React Router's framework features, your application is automatically code split to improve the performance of initial load times when users visit your application.
|
||||
|
||||
## Code Splitting by Route
|
||||
|
||||
Consider this simple route config:
|
||||
|
||||
```tsx filename=app/routes.ts
|
||||
import {
|
||||
type RouteConfig,
|
||||
route,
|
||||
} from "@react-router/dev/routes";
|
||||
|
||||
export default [
|
||||
route("/contact", "./contact.tsx"),
|
||||
route("/about", "./about.tsx"),
|
||||
] satisfies RouteConfig;
|
||||
```
|
||||
|
||||
Instead of bundling all routes into a single giant build, the modules referenced (`contact.tsx` and `about.tsx`) become entry points to the bundler.
|
||||
|
||||
Because these entry points are coupled to URL segments, React Router knows just from a URL which bundles are needed in the browser, and more importantly, which are not.
|
||||
|
||||
If the user visits `"/about"` then the bundles for `about.tsx` will be loaded but not `contact.tsx`. This drastically reduces the JavaScript footprint for initial page loads and speeds up your application.
|
||||
|
||||
## Removal of Server Code
|
||||
|
||||
Any server-only [Route Module APIs][route-module] will be removed from the bundles. Consider this route module:
|
||||
|
||||
```tsx
|
||||
export async function loader() {
|
||||
return { message: "hello" };
|
||||
}
|
||||
|
||||
export async function action() {
|
||||
console.log(Date.now());
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export async function headers() {
|
||||
return { "Cache-Control": "max-age=300" };
|
||||
}
|
||||
|
||||
export default function Component({ loaderData }) {
|
||||
return <div>{loaderData.message}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
After building for the browser, only the `Component` will still be in the bundle, so you can use server-only code in the other module exports.
|
||||
|
||||
[route-module]: ../start/framework/route-module
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
---
|
||||
title: Network Concurrency Management
|
||||
---
|
||||
|
||||
# Network Concurrency Management
|
||||
|
||||
[MODES: framework, data]
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
When building web applications, managing network requests can be a daunting task. The challenges of ensuring up-to-date data and handling simultaneous requests often lead to complex logic in the application to deal with interruptions and race conditions. React Router simplifies this process by automating network management while mirroring and expanding upon the intuitive behavior of web browsers.
|
||||
|
||||
To help understand how React Router handles concurrency, it's important to remember that after `form` submissions, React Router will fetch fresh data from the `loader`s. This is called revalidation.
|
||||
|
||||
## Natural Alignment with Browser Behavior
|
||||
|
||||
React Router's handling of network concurrency is heavily inspired by the default behavior of web browsers when processing documents.
|
||||
|
||||
### Link Navigation
|
||||
|
||||
**Browser Behavior**: When you click on a link in a browser and then click on another before the page transition completes, the browser prioritizes the most recent `action`. It cancels the initial request, focusing solely on the latest link clicked.
|
||||
|
||||
**React Router Behavior**: React Router manages client-side navigation the same way. When a link is clicked within a React Router application, it initiates fetch requests for each `loader` tied to the target URL. If another navigation interrupts the initial navigation, React Router cancels the previous fetch requests, ensuring that only the latest requests proceed.
|
||||
|
||||
### Form Submission
|
||||
|
||||
**Browser Behavior**: If you initiate a form submission in a browser and then quickly submit another form again, the browser disregards the first submission, processing only the latest one.
|
||||
|
||||
**React Router Behavior**: React Router mimics this behavior when working with forms. If a form is submitted and another submission occurs before the first completes, React Router cancels the original fetch requests. It then waits for the latest submission to complete before triggering page revalidation again.
|
||||
|
||||
## Concurrent Submissions and Revalidation
|
||||
|
||||
While standard browsers are limited to one request at a time for navigations and form submissions, React Router elevates this behavior. Unlike navigation, with [`useFetcher`][use_fetcher] multiple requests can be in flight simultaneously.
|
||||
|
||||
React Router is designed to handle multiple form submissions to server `action`s and concurrent revalidation requests efficiently. It ensures that as soon as new data is available, the state is updated promptly. However, React Router also safeguards against potential pitfalls by refraining from committing stale data when other `action`s introduce race conditions.
|
||||
|
||||
For instance, if three form submissions are in progress, and one completes, React Router updates the UI with that data immediately without waiting for the other two so that the UI remains responsive and dynamic. As the remaining submissions finalize, React Router continues to update the UI, ensuring that the most recent data is displayed.
|
||||
|
||||
Using this key:
|
||||
|
||||
- `|`: Submission begins
|
||||
- ✓: Action complete, data revalidation begins
|
||||
- ✅: Revalidated data is committed to the UI
|
||||
- ❌: Request cancelled
|
||||
|
||||
We can visualize this scenario in the following diagram:
|
||||
|
||||
```text
|
||||
submission 1: |----✓-----✅
|
||||
submission 2: |-----✓-----✅
|
||||
submission 3: |-----✓-----✅
|
||||
```
|
||||
|
||||
However, if a subsequent submission's revalidation completes before an earlier one, React Router discards the earlier data, ensuring that only the most up-to-date information is reflected in the UI:
|
||||
|
||||
```text
|
||||
submission 1: |----✓---------❌
|
||||
submission 2: |-----✓-----✅
|
||||
submission 3: |-----✓-----✅
|
||||
```
|
||||
|
||||
Because the revalidation from submission (2) started later and landed earlier than submission (1), the requests from submission (1) are canceled and only the data from submission (2) is committed to the UI. It was requested later, so it's more likely to contain the updated values from both (1) and (2).
|
||||
|
||||
## Potential for Stale Data
|
||||
|
||||
It's unlikely your users will ever experience this, but there are still chances for the user to see stale data in very rare conditions with inconsistent infrastructure. Even though React Router cancels requests for stale data, they will still end up making it to the server. Canceling a request in the browser simply releases browser resources for that request; it can't "catch up" and stop it from getting to the server. In extremely rare conditions, a canceled request may change data after the interrupting `action`'s revalidation lands. Consider this diagram:
|
||||
|
||||
```text
|
||||
👇 interruption with new submission
|
||||
|----❌----------------------✓
|
||||
|-------✓-----✅
|
||||
👆
|
||||
initial request reaches the server
|
||||
after the interrupting submission
|
||||
has completed revalidation
|
||||
```
|
||||
|
||||
The user is now looking at different data than what is on the server. Note that this problem is both extremely rare and exists with default browser behavior, too. The chance of the initial request reaching the server later than both the submission and revalidation of the second is unexpected on any network and server infrastructure. If this is a concern with your infrastructure, you can send timestamps with your form submissions and write server logic to ignore stale submissions.
|
||||
|
||||
## Example
|
||||
|
||||
In UI components like comboboxes, each keystroke can trigger a network request. Managing such rapid, consecutive requests can be tricky, especially when ensuring that the displayed results match the most recent query. However, with React Router, this challenge is automatically handled, ensuring that users see the correct results without developers having to micromanage the network.
|
||||
|
||||
```tsx filename=app/pages/city-search.tsx
|
||||
export async function loader({ request }) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const cities = await searchCities(searchParams.get("q"));
|
||||
return cities;
|
||||
}
|
||||
|
||||
export function CitySearchCombobox() {
|
||||
const fetcher = useFetcher<typeof loader>();
|
||||
|
||||
return (
|
||||
<fetcher.Form action="/city-search">
|
||||
<Combobox aria-label="Cities">
|
||||
<ComboboxInput
|
||||
name="q"
|
||||
onChange={(event) =>
|
||||
// submit the form onChange to get the list of cities
|
||||
fetcher.submit(event.target.form)
|
||||
}
|
||||
/>
|
||||
|
||||
{/* render with the loader's data */}
|
||||
{fetcher.data ? (
|
||||
<ComboboxPopover className="shadow-popup">
|
||||
{fetcher.data.length > 0 ? (
|
||||
<ComboboxList>
|
||||
{fetcher.data.map((city) => (
|
||||
<ComboboxOption
|
||||
key={city.id}
|
||||
value={city.name}
|
||||
/>
|
||||
))}
|
||||
</ComboboxList>
|
||||
) : (
|
||||
<span>No results found</span>
|
||||
)}
|
||||
</ComboboxPopover>
|
||||
) : null}
|
||||
</Combobox>
|
||||
</fetcher.Form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
All the application needs to know is how to query the data and how to render it. React Router handles the network.
|
||||
|
||||
## Conclusion
|
||||
|
||||
React Router offers developers an intuitive, browser-based approach to managing network requests. By mirroring browser behaviors and enhancing them where needed, it simplifies the complexities of concurrency, revalidation, and potential race conditions. Whether you're building a simple webpage or a sophisticated web application, React Router ensures that your user interactions are smooth, reliable, and always up to date.
|
||||
|
||||
[use_fetcher]: ../api/hooks/useFetcher
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
---
|
||||
title: Form vs. fetcher
|
||||
---
|
||||
|
||||
# Form vs. fetcher
|
||||
|
||||
[MODES: framework, data]
|
||||
|
||||
## Overview
|
||||
|
||||
Developing in React Router offers a rich set of tools that can sometimes overlap in functionality, creating a sense of ambiguity for newcomers. The key to effective development in React Router is understanding the nuances and appropriate use cases for each tool. This document seeks to provide clarity on when and why to use specific APIs.
|
||||
|
||||
## APIs in Focus
|
||||
|
||||
- [`<Form>`][form-component]
|
||||
- [`useFetcher`][use-fetcher]
|
||||
- [`useNavigation`][use-navigation]
|
||||
|
||||
Understanding the distinctions and intersections of these APIs is vital for efficient and effective React Router development.
|
||||
|
||||
## URL Considerations
|
||||
|
||||
The primary criterion when choosing among these tools is whether you want the URL to change or not:
|
||||
|
||||
- **URL Change Desired**: When navigating or transitioning between pages, or after certain actions like creating or deleting records. This ensures that the user's browser history accurately reflects their journey through your application.
|
||||
- **Expected Behavior**: In many cases, when users hit the back button, they should be taken to the previous page. Other times the history entry may be replaced but the URL change is important nonetheless.
|
||||
|
||||
- **No URL Change Desired**: For actions that don't significantly change the context or primary content of the current view. This might include updating individual fields or minor data manipulations that don't warrant a new URL or page reload. This also applies to loading data with fetchers for things like popovers, combo boxes, etc.
|
||||
|
||||
### When the URL Should Change
|
||||
|
||||
These actions typically reflect significant changes to the user's context or state:
|
||||
|
||||
- **Creating a New Record**: After creating a new record, it's common to redirect users to a page dedicated to that new record, where they can view or further modify it.
|
||||
|
||||
- **Deleting a Record**: If a user is on a page dedicated to a specific record and decides to delete it, the logical next step is to redirect them to a general page, such as a list of all records.
|
||||
|
||||
For these cases, developers should consider using a combination of [`<Form>`][form-component] and [`useNavigation`][use-navigation]. These tools can be coordinated to handle form submission, invoke specific actions, retrieve action-related data through component props, and manage navigation respectively.
|
||||
|
||||
### When the URL Shouldn't Change
|
||||
|
||||
These actions are generally more subtle and don't require a context switch for the user:
|
||||
|
||||
- **Updating a Single Field**: Maybe a user wants to change the name of an item in a list or update a specific property of a record. This action is minor and doesn't necessitate a new page or URL.
|
||||
|
||||
- **Deleting a Record from a List**: In a list view, if a user deletes an item, they likely expect to remain on the list view, with that item no longer in the list.
|
||||
|
||||
- **Creating a Record in a List View**: When adding a new item to a list, it often makes sense for the user to remain in that context, seeing their new item added to the list without a full page transition.
|
||||
|
||||
- **Loading Data for a Popover or Combobox**: When loading data for a popover or combobox, the user's context remains unchanged. The data is loaded in the background and displayed in a small, self-contained UI element.
|
||||
|
||||
For such actions, [`useFetcher`][use-fetcher] is the go-to API. It's versatile, combining functionalities of these APIs, and is perfectly suited for tasks where the URL should remain unchanged.
|
||||
|
||||
## API Comparison
|
||||
|
||||
As you can see, the two sets of APIs have a lot of similarities:
|
||||
|
||||
| Navigation/URL API | Fetcher API |
|
||||
| ----------------------------- | -------------------- |
|
||||
| `<Form>` | `<fetcher.Form>` |
|
||||
| `actionData` (component prop) | `fetcher.data` |
|
||||
| `navigation.state` | `fetcher.state` |
|
||||
| `navigation.formAction` | `fetcher.formAction` |
|
||||
| `navigation.formData` | `fetcher.formData` |
|
||||
|
||||
## Examples
|
||||
|
||||
### Creating a New Record
|
||||
|
||||
```tsx filename=app/pages/new-recipe.tsx lines=[16,23-24,29]
|
||||
import {
|
||||
Form,
|
||||
redirect,
|
||||
useNavigation,
|
||||
} from "react-router";
|
||||
import type { Route } from "./+types/new-recipe";
|
||||
|
||||
export async function action({
|
||||
request,
|
||||
}: Route.ActionArgs) {
|
||||
const formData = await request.formData();
|
||||
const errors = await validateRecipeFormData(formData);
|
||||
if (errors) {
|
||||
return { errors };
|
||||
}
|
||||
const recipe = await db.recipes.create(formData);
|
||||
return redirect(`/recipes/${recipe.id}`);
|
||||
}
|
||||
|
||||
export function NewRecipe({
|
||||
actionData,
|
||||
}: Route.ComponentProps) {
|
||||
const { errors } = actionData || {};
|
||||
const navigation = useNavigation();
|
||||
const isSubmitting =
|
||||
navigation.formAction === "/recipes/new";
|
||||
|
||||
return (
|
||||
<Form method="post">
|
||||
<label>
|
||||
Title: <input name="title" />
|
||||
{errors?.title ? <span>{errors.title}</span> : null}
|
||||
</label>
|
||||
<label>
|
||||
Ingredients: <textarea name="ingredients" />
|
||||
{errors?.ingredients ? (
|
||||
<span>{errors.ingredients}</span>
|
||||
) : null}
|
||||
</label>
|
||||
<label>
|
||||
Directions: <textarea name="directions" />
|
||||
{errors?.directions ? (
|
||||
<span>{errors.directions}</span>
|
||||
) : null}
|
||||
</label>
|
||||
<button type="submit">
|
||||
{isSubmitting ? "Saving..." : "Create Recipe"}
|
||||
</button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
The example leverages [`<Form>`][form-component], component props, and [`useNavigation`][use-navigation] to facilitate an intuitive record creation process.
|
||||
|
||||
Using `<Form>` ensures direct and logical navigation. After creating a record, the user is naturally guided to the new recipe's unique URL, reinforcing the outcome of their action.
|
||||
|
||||
The component props bridge server and client, providing immediate feedback on submission issues. This quick response enables users to rectify any errors without hindrance.
|
||||
|
||||
Lastly, `useNavigation` dynamically reflects the form's submission state. This subtle UI change, like toggling the button's label, assures users that their actions are being processed.
|
||||
|
||||
Combined, these APIs offer a balanced blend of structured navigation and feedback.
|
||||
|
||||
### Updating a Record
|
||||
|
||||
Now consider we're looking at a list of recipes that have delete buttons on each item. When a user clicks the delete button, we want to delete the recipe from the database and remove it from the list without navigating away from the list.
|
||||
|
||||
First, consider the basic route setup to get a list of recipes on the page:
|
||||
|
||||
```tsx filename=app/pages/recipes.tsx
|
||||
import type { Route } from "./+types/recipes";
|
||||
|
||||
export async function loader({
|
||||
request,
|
||||
}: Route.LoaderArgs) {
|
||||
return {
|
||||
recipes: await db.recipes.findAll({ limit: 30 }),
|
||||
};
|
||||
}
|
||||
|
||||
export default function Recipes({
|
||||
loaderData,
|
||||
}: Route.ComponentProps) {
|
||||
const { recipes } = loaderData;
|
||||
return (
|
||||
<ul>
|
||||
{recipes.map((recipe) => (
|
||||
<RecipeListItem key={recipe.id} recipe={recipe} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Now we'll look at the action that deletes a recipe and the component that renders each recipe in the list.
|
||||
|
||||
```tsx filename=app/pages/recipes.tsx lines=[10,21,27]
|
||||
import { useFetcher } from "react-router";
|
||||
import type { Recipe } from "./recipe.server";
|
||||
import type { Route } from "./+types/recipes";
|
||||
|
||||
export async function action({
|
||||
request,
|
||||
}: Route.ActionArgs) {
|
||||
const formData = await request.formData();
|
||||
const id = formData.get("id");
|
||||
await db.recipes.delete(id);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export default function Recipes() {
|
||||
return (
|
||||
// ...
|
||||
// doesn't matter, somewhere it's using <RecipeListItem />
|
||||
)
|
||||
}
|
||||
|
||||
function RecipeListItem({ recipe }: { recipe: Recipe }) {
|
||||
const fetcher = useFetcher();
|
||||
const isDeleting = fetcher.state !== "idle";
|
||||
|
||||
return (
|
||||
<li>
|
||||
<h2>{recipe.title}</h2>
|
||||
<fetcher.Form method="post">
|
||||
<input type="hidden" name="id" value={recipe.id} />
|
||||
<button disabled={isDeleting} type="submit">
|
||||
{isDeleting ? "Deleting..." : "Delete"}
|
||||
</button>
|
||||
</fetcher.Form>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Using [`useFetcher`][use-fetcher] in this scenario works perfectly. Instead of navigating away or refreshing the entire page, we want in-place updates. When a user deletes a recipe, the `action` is called and the fetcher manages the corresponding state transitions.
|
||||
|
||||
The key advantage here is the maintenance of context. The user stays on the list when the deletion completes. The fetcher's state management capabilities are leveraged to give real-time feedback: it toggles between `"Deleting..."` and `"Delete"`, providing a clear indication of the ongoing process.
|
||||
|
||||
Furthermore, with each `fetcher` having the autonomy to manage its own state, operations on individual list items become independent, ensuring that actions on one item don't affect the others (though revalidation of the page data is a shared concern covered in [Network Concurrency Management][network-concurrency-management]).
|
||||
|
||||
In essence, `useFetcher` offers a seamless mechanism for actions that don't necessitate a change in the URL or navigation, enhancing the user experience by providing real-time feedback and context preservation.
|
||||
|
||||
### Mark Article as Read
|
||||
|
||||
Imagine you want to mark that an article has been read by the current user, after they've been on the page for a while and scrolled to the bottom. You could make a hook that looks something like this:
|
||||
|
||||
```tsx
|
||||
import { useFetcher } from "react-router";
|
||||
|
||||
function useMarkAsRead({ articleId, userId }) {
|
||||
const marker = useFetcher();
|
||||
|
||||
useSpentSomeTimeHereAndScrolledToTheBottom(() => {
|
||||
marker.submit(
|
||||
{ userId },
|
||||
{
|
||||
action: `/article/${articleId}/mark-as-read`,
|
||||
method: "post",
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### User Avatar Details Popup
|
||||
|
||||
Anytime you show the user avatar, you could put a hover effect that fetches data from a loader and displays it in a popup.
|
||||
|
||||
```tsx filename=app/pages/user-details.tsx
|
||||
import { useState, useEffect } from "react";
|
||||
import { useFetcher } from "react-router";
|
||||
import type { Route } from "./+types/user-details";
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
return await fakeDb.user.find({
|
||||
where: { id: params.id },
|
||||
});
|
||||
}
|
||||
|
||||
type LoaderData = Route.ComponentProps["loaderData"];
|
||||
|
||||
function UserAvatar({ partialUser }) {
|
||||
const userDetails = useFetcher<LoaderData>();
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
showDetails &&
|
||||
userDetails.state === "idle" &&
|
||||
!userDetails.data
|
||||
) {
|
||||
userDetails.load(`/user-details/${partialUser.id}`);
|
||||
}
|
||||
}, [showDetails, userDetails, partialUser.id]);
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => setShowDetails(true)}
|
||||
onMouseLeave={() => setShowDetails(false)}
|
||||
>
|
||||
<img src={partialUser.profileImageUrl} />
|
||||
{showDetails ? (
|
||||
userDetails.state === "idle" && userDetails.data ? (
|
||||
<UserPopup user={userDetails.data} />
|
||||
) : (
|
||||
<UserPopupLoading />
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
React Router offers a range of tools to cater to varied developmental needs. While some functionalities might seem to overlap, each tool has been crafted with specific scenarios in mind. By understanding the intricacies and ideal applications of `<Form>`, `useFetcher`, and `useNavigation`, along with how data flows through component props, developers can create more intuitive, responsive, and user-friendly web applications.
|
||||
|
||||
[form-component]: ../api/components/Form
|
||||
[use-fetcher]: ../api/hooks/useFetcher
|
||||
[use-navigation]: ../api/hooks/useNavigation
|
||||
[network-concurrency-management]: ./concurrency
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
---
|
||||
title: Hot Module Replacement
|
||||
---
|
||||
|
||||
# Hot Module Replacement
|
||||
|
||||
[MODES: framework]
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
Hot Module Replacement is a technique for updating modules in your app without needing to reload the page.
|
||||
It's a great developer experience, and React Router supports it when using Vite.
|
||||
|
||||
HMR does its best to preserve browser state across updates.
|
||||
For example, let's say you have form within a modal and you fill out all the fields.
|
||||
As soon as you save any changes to the code, traditional live reload would hard refresh the page causing all of those fields to be reset.
|
||||
Every time you make a change, you'd have to open up the modal _again_ and fill out the form _again_.
|
||||
|
||||
But with HMR, all of that state is preserved _across updates_.
|
||||
|
||||
## React Fast Refresh
|
||||
|
||||
React already has mechanisms for updating the DOM via its [virtual DOM][virtual-dom] in response to user interactions like clicking a button.
|
||||
Wouldn't it be great if React could handle updating the DOM in response to code changes too?
|
||||
|
||||
That's exactly what [React Fast Refresh][react-refresh] is all about!
|
||||
Of course, React is all about components, not general JavaScript code, so React Fast Refresh only handles hot updates for exported React components.
|
||||
|
||||
But React Fast Refresh does have some limitations that you should be aware of.
|
||||
|
||||
### Class Component State
|
||||
|
||||
React Fast Refresh does not preserve state for class components.
|
||||
This includes higher-order components that internally return classes:
|
||||
|
||||
```tsx
|
||||
export class ComponentA extends Component {} // ❌
|
||||
|
||||
export const ComponentB = HOC(ComponentC); // ❌ Won't work if HOC returns a class component
|
||||
|
||||
export function ComponentD() {} // ✅
|
||||
export const ComponentE = () => {}; // ✅
|
||||
export default function ComponentF() {} // ✅
|
||||
```
|
||||
|
||||
### Named Function Components
|
||||
|
||||
Function components must be named, not anonymous, for React Fast Refresh to track changes:
|
||||
|
||||
```tsx
|
||||
export default () => {}; // ❌
|
||||
export default function () {} // ❌
|
||||
|
||||
const ComponentA = () => {};
|
||||
export default ComponentA; // ✅
|
||||
|
||||
export default function ComponentB() {} // ✅
|
||||
```
|
||||
|
||||
### Supported Exports
|
||||
|
||||
React Fast Refresh can only handle component exports. While React Router manages [route exports like `action`, ` headers`, `links`, `loader`, and `meta`][route-module] for you, any user-defined exports will cause full reloads:
|
||||
|
||||
```tsx
|
||||
// These exports are handled by the React Router Vite plugin
|
||||
// to be HMR-compatible
|
||||
export const meta = { title: "Home" }; // ✅
|
||||
export const links = [
|
||||
{ rel: "stylesheet", href: "style.css" },
|
||||
]; // ✅
|
||||
|
||||
// These exports are removed by the React Router Vite plugin
|
||||
// so they never affect HMR
|
||||
export const headers = { "Cache-Control": "max-age=3600" }; // ✅
|
||||
export const loader = async () => {}; // ✅
|
||||
export const action = async () => {}; // ✅
|
||||
|
||||
// This is not a route module export, nor a component export,
|
||||
// so it will cause a full reload for this route
|
||||
export const myValue = "some value"; // ❌
|
||||
|
||||
export default function Route() {} // ✅
|
||||
```
|
||||
|
||||
👆 Routes probably shouldn't be exporting random values like that anyway.
|
||||
If you want to reuse values across routes, stick them in their own non-route module:
|
||||
|
||||
```ts filename=my-custom-value.ts
|
||||
export const myValue = "some value";
|
||||
```
|
||||
|
||||
### Changing Hooks
|
||||
|
||||
React Fast Refresh cannot track changes for a component when hooks are being added or removed from it, causing full reloads just for the next render. After the hooks have been updated, changes should result in hot updates again. For example, if you add a `useState` to your component, you may lose that component's local state for the next render.
|
||||
|
||||
Additionally, if you are destructuring a hook's return value, React Fast Refresh will not be able to preserve state for the component if the destructured key is removed or renamed.
|
||||
For example:
|
||||
|
||||
```tsx
|
||||
export default function Component({ loaderData }) {
|
||||
const { pet } = useMyCustomHook();
|
||||
return (
|
||||
<div>
|
||||
<input />
|
||||
<p>My dog's name is {pet.name}!</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
If you change the key `pet` to `dog`:
|
||||
|
||||
```diff
|
||||
export default function Component() {
|
||||
- const { pet } = useMyCustomHook();
|
||||
+ const { dog } = useMyCustomHook();
|
||||
return (
|
||||
<div>
|
||||
<input />
|
||||
- <p>My dog's name is {pet.name}!</p>
|
||||
+ <p>My dog's name is {dog.name}!</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
then React Fast Refresh will not be able to preserve state `<input />` ❌.
|
||||
|
||||
### Component Keys
|
||||
|
||||
In some cases, React cannot distinguish between existing components being changed and new components being added. [React needs `key`s][react-keys] to disambiguate these cases and track changes when sibling elements are modified.
|
||||
|
||||
[virtual-dom]: https://reactjs.org/docs/faq-internals.html#what-is-the-virtual-dom
|
||||
[react-refresh]: https://github.com/facebook/react/tree/main/packages/react-refresh
|
||||
[react-keys]: https://react.dev/learn/rendering-lists#why-does-react-need-keys
|
||||
[route-module]: ../start/framework/route-module
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: Hydration
|
||||
hidden: true
|
||||
---
|
||||
|
||||
There are a few nuances worth noting around the behavior of `HydrateFallback`:
|
||||
|
||||
- It is only relevant on initial document request and hydration, and will not be rendered on any subsequent client-side navigations
|
||||
- It is only relevant when you are also setting [`clientLoader.hydrate=true`][hydrate-true] on a given route
|
||||
- It is also relevant if you do have a `clientLoader` without a server `loader`, as this implies `clientLoader.hydrate=true` since there is otherwise no loader data at all to return from `useLoaderData`
|
||||
- Even if you do not specify a `HydrateFallback` in this case, React Router will not render your route component and will bubble up to any ancestor `HydrateFallback` component
|
||||
- This is to ensure that `useLoaderData` remains "happy-path"
|
||||
- Without a server `loader`, `useLoaderData` would return `undefined` in any rendered route components
|
||||
- You cannot render an `<Outlet/>` in a `HydrateFallback` because children routes can't be guaranteed to operate correctly since their ancestor loader data may not yet be available if they are running `clientLoader` functions on hydration (i.e., use cases such as `useRouteLoaderData()` or `useMatches()`)
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
---
|
||||
title: Index Query Param
|
||||
---
|
||||
|
||||
# Index Query Param
|
||||
|
||||
[MODES: framework, data]
|
||||
|
||||
## Overview
|
||||
|
||||
You may find a wild `?index` appearing in the URL of your app when submitting forms.
|
||||
|
||||
Because of nested routes, multiple routes in your route hierarchy can match the URL. Unlike navigations where all matching route [`loader`][loader]s are called to build up the UI, when a [`form`][form_element] is submitted, _only one action is called_.
|
||||
|
||||
Because index routes share the same URL as their parent, the `?index` param lets you disambiguate between the two.
|
||||
|
||||
## Understanding Index Routes
|
||||
|
||||
For example, consider the following route structure:
|
||||
|
||||
```ts filename=app/routes.ts
|
||||
import {
|
||||
type RouteConfig,
|
||||
route,
|
||||
index,
|
||||
} from "@react-router/dev/routes";
|
||||
|
||||
export default [
|
||||
route("projects", "./pages/projects.tsx", [
|
||||
index("./pages/projects/index.tsx"),
|
||||
route(":id", "./pages/projects/project.tsx"),
|
||||
]),
|
||||
] satisfies RouteConfig;
|
||||
```
|
||||
|
||||
This creates two routes that match `/projects`:
|
||||
|
||||
- The parent route (`./pages/projects.tsx`)
|
||||
- The index route (`./pages/projects/index.tsx`)
|
||||
|
||||
## Form Submission Targeting
|
||||
|
||||
For example, consider the following forms:
|
||||
|
||||
```tsx
|
||||
<Form method="post" action="/projects" />
|
||||
<Form method="post" action="/projects?index" />
|
||||
```
|
||||
|
||||
The `?index` param will submit to the index route; the action without the index param will submit to the parent route.
|
||||
|
||||
When a [`<Form>`][form_component] is rendered in an index route without an [`action`][action], the `?index` param will automatically be appended so that the form posts to the index route. The following form, when submitted, will post to `/projects?index` because it is rendered in the context of the `projects` index route:
|
||||
|
||||
```tsx filename=app/pages/projects/index.tsx
|
||||
function ProjectsIndex() {
|
||||
return <Form method="post" />;
|
||||
}
|
||||
```
|
||||
|
||||
If you moved the code to the project layout (`./pages/projects.tsx` in this example), it would instead post to `/projects`.
|
||||
|
||||
This applies to `<Form>` and all of its cousins:
|
||||
|
||||
```tsx
|
||||
function Component() {
|
||||
const submit = useSubmit();
|
||||
submit({}, { action: "/projects" });
|
||||
submit({}, { action: "/projects?index" });
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
function Component() {
|
||||
const fetcher = useFetcher();
|
||||
fetcher.submit({}, { action: "/projects" });
|
||||
fetcher.submit({}, { action: "/projects?index" });
|
||||
<fetcher.Form action="/projects" />;
|
||||
<fetcher.Form action="/projects?index" />;
|
||||
<fetcher.Form />; // defaults to the route in context
|
||||
}
|
||||
```
|
||||
|
||||
[loader]: ../start/data/route-object#loader
|
||||
[form_element]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form
|
||||
[form_component]: ../api/components/Form
|
||||
[action]: ../start/data/route-object#action
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: Explanations
|
||||
order: 5
|
||||
---
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
---
|
||||
title: Lazy Route Discovery
|
||||
---
|
||||
|
||||
# Lazy Route Discovery
|
||||
|
||||
[MODES: framework]
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
Lazy Route Discovery is a performance optimization that loads route information progressively as users navigate through your application, rather than loading the complete route manifest upfront.
|
||||
|
||||
With Lazy Route Discovery enabled (the default), React Router sends only the routes needed for the initial server-side render in the manifest. As users navigate to new parts of your application, additional route information is fetched dynamically and added to the client-side manifest.
|
||||
|
||||
The route manifest contains metadata about your routes (JavaScript/CSS imports, whether routes have `loaders`/`actions`, etc.) but not the actual route module implementations. This allows React Router to understand your application's structure without downloading unnecessary route information.
|
||||
|
||||
## Route Discovery Process
|
||||
|
||||
When a user navigates to a new route that isn't in the current manifest:
|
||||
|
||||
1. **Route Discovery Request** - React Router makes a request to the internal `/__manifest` endpoint
|
||||
2. **Manifest Patch** - The server responds with the required route information
|
||||
3. **Route Loading** - React Router loads the necessary route modules and data
|
||||
4. **Navigation** - The user navigates to the new route
|
||||
|
||||
## Eager Discovery Optimization
|
||||
|
||||
To prevent navigation waterfalls, React Router implements eager route discovery. All [`<Link>`](../api/components/Link) and [`<NavLink>`](../api/components/NavLink) components rendered on the current page are automatically discovered via a batched request to the server.
|
||||
|
||||
This discovery request typically completes before users click any links, making subsequent navigation feel synchronous even with lazy route discovery enabled.
|
||||
|
||||
```tsx
|
||||
// Links are automatically discovered by default
|
||||
<Link to="/dashboard">Dashboard</Link>
|
||||
|
||||
// Opt out of discovery for specific links
|
||||
<Link to="/admin" discover="none">Admin</Link>
|
||||
```
|
||||
|
||||
## Performance Benefits
|
||||
|
||||
Lazy Route Discovery provides several performance improvements:
|
||||
|
||||
- **Faster Initial Load** - Smaller initial bundle size by excluding unused route metadata
|
||||
- **Reduced Memory Usage** - Route information is loaded only when needed
|
||||
- **Scalability** - Applications with hundreds of routes see more significant benefits
|
||||
|
||||
## Configuration
|
||||
|
||||
You can configure route discovery behavior in your `react-router.config.ts`:
|
||||
|
||||
```tsx filename=react-router.config.ts
|
||||
export default {
|
||||
// Default: lazy discovery with /__manifest endpoint
|
||||
routeDiscovery: {
|
||||
mode: "lazy",
|
||||
manifestPath: "/__manifest",
|
||||
},
|
||||
|
||||
// Custom manifest path (useful for multiple apps on same domain)
|
||||
routeDiscovery: {
|
||||
mode: "lazy",
|
||||
manifestPath: "/my-app-manifest",
|
||||
},
|
||||
|
||||
// Disable lazy discovery (include all routes initially)
|
||||
routeDiscovery: { mode: "initial" },
|
||||
} satisfies Config;
|
||||
```
|
||||
|
||||
## Deployment Considerations
|
||||
|
||||
When using lazy route discovery, ensure your deployment setup handles manifest requests properly:
|
||||
|
||||
- **Route Handling** - Ensure `/__manifest` requests reach your React Router handler
|
||||
- **CDN Caching** - If using CDN/edge caching, include `version` and `paths` query parameters in your cache key for the manifest endpoint
|
||||
- **Multiple Applications** - Use a custom `manifestPath` if running multiple React Router applications on the same domain
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Location Object
|
||||
hidden: true
|
||||
---
|
||||
|
||||
<!-- put some stuff about what it is and how it can be used, probably good opportunity for a couple how-tos as well with scroll restoration, etc -->
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
---
|
||||
title: Progressive Enhancement
|
||||
---
|
||||
|
||||
# Progressive Enhancement
|
||||
|
||||
[MODES: framework]
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
> Progressive enhancement is a strategy in web design that puts emphasis on web content first, allowing everyone to access the basic content and functionality of a web page, whilst users with additional browser features or faster Internet access receive the enhanced version instead.
|
||||
>
|
||||
> <cite>- [Wikipedia][wikipedia]</cite>
|
||||
|
||||
When using React Router with Server-Side Rendering (the default in framework mode), you can automatically leverage the benefits of progressive enhancement.
|
||||
|
||||
## Why Progressive Enhancement Matters
|
||||
|
||||
Coined in 2003 by Steven Champeon & Nick Finck, the phrase emerged during a time of varied CSS and JavaScript support across different browsers, with many users actually browsing the web with JavaScript disabled.
|
||||
|
||||
Today, we are fortunate to develop for a much more consistent web and where the majority of users have JavaScript enabled.
|
||||
|
||||
However, we still believe in the core principles of progressive enhancement in React Router. It leads to fast and resilient apps with simple development workflows.
|
||||
|
||||
**Performance**: While it's easy to think that only 5% of your users have slow connections, the reality is that 100% of your users have slow connections 5% of the time.
|
||||
|
||||
**Resilience**: Everybody has JavaScript disabled until it's loaded.
|
||||
|
||||
**Simplicity**: Building your apps in a progressively enhanced way with React Router is actually simpler than building a traditional SPA.
|
||||
|
||||
## Performance
|
||||
|
||||
Server rendering allows your app to do more things in parallel than a typical [Single Page App (SPA)][spa], making the initial loading experience and subsequent navigations faster.
|
||||
|
||||
Typical SPAs send a blank document and only start doing work when JavaScript has loaded:
|
||||
|
||||
```
|
||||
HTML |---|
|
||||
JavaScript |---------|
|
||||
Data |---------------|
|
||||
page rendered 👆
|
||||
```
|
||||
|
||||
A React Router app can start doing work the moment the request hits the server and stream the response so that the browser can start downloading JavaScript, other assets, and data in parallel:
|
||||
|
||||
```
|
||||
👇 first byte
|
||||
HTML |---|-----------|
|
||||
JavaScript |---------|
|
||||
Data |---------------|
|
||||
page rendered 👆
|
||||
```
|
||||
|
||||
## Resilience and Accessibility
|
||||
|
||||
While your users probably don't browse the web with JavaScript disabled, everybody uses the websites without JavaScript before it finishes loading. React Router embraces progressive enhancement by building on top of HTML, allowing you to build your app in a way that works without JavaScript, and then layer on JavaScript to enhance the experience.
|
||||
|
||||
The simplest case is a `<Link to="/account">`. These render an `<a href="/account">` tag that works without JavaScript. When JavaScript loads, React Router will intercept clicks and handle the navigation with client side routing. This gives you more control over the UX instead of just spinning favicons in the browser tab--but it works either way.
|
||||
|
||||
Now consider a simple add to cart button:
|
||||
|
||||
```tsx
|
||||
export function AddToCart({ id }) {
|
||||
return (
|
||||
<Form method="post" action="/add-to-cart">
|
||||
<input type="hidden" name="id" value={id} />
|
||||
<button type="submit">Add To Cart</button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Whether JavaScript has loaded or not doesn't matter, this button will add the product to the cart.
|
||||
|
||||
When JavaScript loads, React Router will intercept the form submission and handle it client side. This allows you to add your own pending UI, or other client side behavior.
|
||||
|
||||
## Simplicity
|
||||
|
||||
When you start to rely on basic features of the web like HTML and URLs, you will find that you reach for client side state and state management much less.
|
||||
|
||||
Consider the button from before, with no fundamental change to the code, we can pepper in some client side behavior:
|
||||
|
||||
```tsx lines=[1,4,7,10-12,14]
|
||||
import { useFetcher } from "react-router";
|
||||
|
||||
export function AddToCart({ id }) {
|
||||
const fetcher = useFetcher();
|
||||
|
||||
return (
|
||||
<fetcher.Form method="post" action="/add-to-cart">
|
||||
<input name="id" value={id} />
|
||||
<button type="submit">
|
||||
{fetcher.state === "submitting"
|
||||
? "Adding..."
|
||||
: "Add To Cart"}
|
||||
</button>
|
||||
</fetcher.Form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
This feature continues to work the very same as it did before when JavaScript is loading, but once JavaScript loads:
|
||||
|
||||
- `useFetcher` no longer causes a navigation like `<Form>` does, so the user can stay on the same page and keep shopping
|
||||
- The app code determines the pending UI instead of spinning favicons in the browser
|
||||
|
||||
It's not about building it two different ways–once for JavaScript and once without–it's about building it in iterations. Start with the simplest version of the feature and ship it; then iterate to an enhanced user experience.
|
||||
|
||||
Not only will the user get a progressively enhanced experience, but the app developer gets to "progressively enhance" the UI without changing the fundamental design of the feature.
|
||||
|
||||
Another example where progressive enhancement leads to simplicity is with the URL. When you start with a URL, you don't need to worry about client side state management. You can just use the URL as the source of truth for the UI.
|
||||
|
||||
```tsx
|
||||
export function SearchBox() {
|
||||
return (
|
||||
<Form method="get" action="/search">
|
||||
<input type="search" name="query" />
|
||||
<SearchIcon />
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
This component doesn't need any state management. It just renders a form that submits to `/search`. When JavaScript loads, React Router will intercept the form submission and handle it client side. Here's the next iteration:
|
||||
|
||||
```tsx lines=[1,4-6,11]
|
||||
import { useNavigation } from "react-router";
|
||||
|
||||
export function SearchBox() {
|
||||
const navigation = useNavigation();
|
||||
const isSearching =
|
||||
navigation.location.pathname === "/search";
|
||||
|
||||
return (
|
||||
<Form method="get" action="/search">
|
||||
<input type="search" name="query" />
|
||||
{isSearching ? <Spinner /> : <SearchIcon />}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
No fundamental change in architecture, simply a progressive enhancement for both the user and the code.
|
||||
|
||||
See also: [State Management][state_management]
|
||||
|
||||
[wikipedia]: https://en.wikipedia.org/wiki/Progressive_enhancement
|
||||
[spa]: ../how-to/spa
|
||||
[state_management]: ./state-management
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
---
|
||||
title: Race Conditions
|
||||
---
|
||||
|
||||
# Race Conditions
|
||||
|
||||
[MODES: framework, data]
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
While impossible to eliminate every possible race condition in your application, React Router automatically handles the most common race conditions found in web user interfaces.
|
||||
|
||||
## Browser Behavior
|
||||
|
||||
React Router's handling of network concurrency is heavily inspired by the behavior of web browsers when processing documents.
|
||||
|
||||
Consider clicking a link to a new document, and then clicking a different link before the new page has finished loading. The browser will:
|
||||
|
||||
1. cancel the first request
|
||||
2. immediately process the new navigation
|
||||
|
||||
The same behavior applies to form submissions. When a pending form submission is interrupted by a new one, the first is canceled and the new submission is immediately processed.
|
||||
|
||||
## React Router Behavior
|
||||
|
||||
Like the browser, interrupted navigations with links and form submissions will cancel in flight data requests and immediately process the new event.
|
||||
|
||||
Fetchers are a bit more nuanced since they are not singleton events like navigation. Fetchers can't interrupt other fetcher instances, but they can interrupt themselves and the behavior is the same as everything else: cancel the interrupted request and immediately process the new one.
|
||||
|
||||
Fetchers do, however, interact with each other when it comes to revalidation. After a fetcher's action request returns to the browser, a revalidation for all page data is sent. This means multiple revalidation requests can be in-flight at the same time. React Router will commit all "fresh" revalidation responses and cancel any stale requests. A stale request is any request that started _earlier_ than one that has returned.
|
||||
|
||||
This management of the network prevents the most common UI bugs caused by network race conditions.
|
||||
|
||||
Since networks are unpredictable, and your server still processes these cancelled requests, your backend may still experience race conditions and have potential data integrity issues. These risks are the same risks as using default browser behavior with plain HTML `<forms>`, which we consider to be low, and outside the scope of React Router.
|
||||
|
||||
## Practical Benefits
|
||||
|
||||
Consider building a type-ahead combobox. As the user types, you send a request to the server. As they type each new character you send a new request. It's important to not show the user results for a value that's not in the text field anymore.
|
||||
|
||||
When using a fetcher, this is automatically managed for you. Consider this pseudo-code:
|
||||
|
||||
```tsx
|
||||
// route("/city-search", "./search-cities.ts")
|
||||
export async function loader({ request }) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
return searchCities(searchParams.get("q"));
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
export function CitySearchCombobox() {
|
||||
const fetcher = useFetcher();
|
||||
|
||||
return (
|
||||
<fetcher.Form action="/city-search">
|
||||
<Combobox aria-label="Cities">
|
||||
<ComboboxInput
|
||||
name="q"
|
||||
onChange={(event) =>
|
||||
// submit the form onChange to get the list of cities
|
||||
fetcher.submit(event.target.form)
|
||||
}
|
||||
/>
|
||||
|
||||
{fetcher.data ? (
|
||||
<ComboboxPopover className="shadow-popup">
|
||||
{fetcher.data.length > 0 ? (
|
||||
<ComboboxList>
|
||||
{fetcher.data.map((city) => (
|
||||
<ComboboxOption
|
||||
key={city.id}
|
||||
value={city.name}
|
||||
/>
|
||||
))}
|
||||
</ComboboxList>
|
||||
) : (
|
||||
<span>No results found</span>
|
||||
)}
|
||||
</ComboboxPopover>
|
||||
) : null}
|
||||
</Combobox>
|
||||
</fetcher.Form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Calls to `fetcher.submit` will cancel pending requests on that fetcher automatically. This ensures you never show the user results for a request for a different input value.
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
---
|
||||
title: React Transitions
|
||||
---
|
||||
|
||||
# React Transitions
|
||||
|
||||
[MODES: framework, data, declarative]
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
[React 18][react-18] introduced the concept of "transitions" which allow you to differentiate urgent from non-urgent UI updates. To learn more about React Transitions and "concurrent rendering" Please refer to React's official documentation:
|
||||
|
||||
- [What is Concurrent React][concurrent]
|
||||
- [Transitions][transitions]
|
||||
- [`React.useTransition`][use-transition]
|
||||
- [`React.startTransition`][start-transition]
|
||||
|
||||
[React 19][react-19] enhances the async/concurrent landscape by introducing [Actions][actions] and support for using async functions in Transitions. With the support for async Transitions, a new [`React.useOptimistic`][use-optimistic-blog] [hook][use-optimistic] was also introduced that allows you to surface state updates during a Transition to show users instant feedback.
|
||||
|
||||
## Transitions in React Router
|
||||
|
||||
The introduction of Transitions in React makes the story of how React Router manages your navigations and router state a bit more complicated. These are powerful APIs but they don't come without some nuance and added complexity. We aim to make React Router work seamlessly with the new React features, but in some cases there may exist some tension between the new React ways to do things and some patterns you are already using in your React Router apps (i.e., pending states, optimistic UI).
|
||||
|
||||
To ensure a smooth adoption story, we've introduced changes related to Transitions behind an opt-in `useTransitions` flag so that you can upgrade in a non-breaking fashion.
|
||||
|
||||
### Current Behavior
|
||||
|
||||
We first leveraged `React.startTransition` to make React Router more Suspense-friendly in React Router [6.13.0][rr-6-13-0] via the `future.v7_startTransition` flag. In v7, that became the default behavior and all router state updates are currently wrapped in `React.startTransition`.
|
||||
|
||||
This default behavior has 2 potential issues that `useTransitions` is designed to solve:
|
||||
|
||||
- There are some valid use cases where you _don't_ want your updates wrapped in `startTransition`
|
||||
- One specific issue is that `React.useSyncExternalStore` updates can't be Transitions ([^1][uses-transition-issue], [^2][uses-transition-tweet]). `useSyncExternalStore` forces a sync update, which means fallbacks can be shown in update transitions that would otherwise avoid showing the fallback.
|
||||
- React Router has a `flushSync` option on navigations to use [`React.flushSync`][flush-sync] for state updates instead, but that's not always a proper solution
|
||||
- React 19 has added a new `startTransition(() => Promise))` API as well as a new `useOptimistic` hook to surface updates during Transitions
|
||||
- Without some updates to React Router, `startTransition(() => navigate(path))` doesn't work as you might expect, because we are not using `useOptimistic` internally so router state updates don't surface during the navigation, which breaks hooks like `useNavigation`
|
||||
|
||||
To provide a solution to both of the above issues, we're introducing a new `useTransitions` prop to the router components that will let you opt-out of using `startTransition` for router state updates (solving the first issue), or opt-into a more enhanced usage of `startTransition` + `useOptimistic` (solving the second issue). Because the current behavior is a bit incomplete with the new React 19 APIs, we plan to make the opt-in behavior the default in React Router v8, but we will likely retain the opt-out flag for use cases such as `useSyncExternalStore`.
|
||||
|
||||
### Opt-out via `useTransitions=false`
|
||||
|
||||
If your application is not "Transition-friendly" due to the usage of `useSyncExternalStore` (or other reasons), then you can opt-out via the prop:
|
||||
|
||||
```tsx
|
||||
// Framework Mode (entry.client.tsx)
|
||||
<HydratedRouter useTransitions={false} />
|
||||
|
||||
// Data Mode
|
||||
<RouterProvider useTransitions={false} />
|
||||
|
||||
// Declarative Mode
|
||||
<BrowserRouter useTransitions={false} />
|
||||
```
|
||||
|
||||
This will stop the router from wrapping internal state updates in `startTransition`.
|
||||
|
||||
### Opt-in via `useTransitions=true`
|
||||
|
||||
<docs-info>Opting into this feature in Framework or Data Mode requires that you are using React 19 because it needs access to [`React.useOptimistic`][use-optimistic]</docs-info>
|
||||
|
||||
If you want to make your application play nicely with all of the new React 19 features that rely on concurrent mode and Transitions, then you can opt-in via the new prop:
|
||||
|
||||
```tsx
|
||||
// Framework Mode (entry.client.tsx)
|
||||
<HydratedRouter useTransitions />
|
||||
|
||||
// Data Mode
|
||||
<RouterProvider useTransitions />
|
||||
|
||||
// Declarative Mode
|
||||
<BrowserRouter useTransitions />
|
||||
```
|
||||
|
||||
With this flag enabled:
|
||||
|
||||
- All internal state updates are wrapped in `React.startTransition` (current behavior without the flag)
|
||||
- All `<Link>`/`<Form>` navigations will be wrapped in `React.startTransition`, using the promise returned by `useNavigate`/`useSubmit` so that the Transition lasts for the duration of the navigation
|
||||
- `useNavigate`/`useSubmit` do not automatically wrap in `React.startTransition`, so you can opt-out of a Transition-enabled navigation by using those directly
|
||||
- In Framework/Data modes, a subset of the router state updates during a navigation will be surfaced to the UI via `useOptimistic`
|
||||
- State related to the _ongoing_ navigation and all fetcher information will be surfaced:
|
||||
- `state.navigation` for `useNavigation()`
|
||||
- `state.revalidation` for `useRevalidator()`
|
||||
- `state.actionData` for `useActionData()`
|
||||
- `state.fetchers` for `useFetcher()` and `useFetchers()`
|
||||
- State related to the _current_ location will not be surfaced:
|
||||
- `state.location` for `useLocation`
|
||||
- `state.matches` for `useMatches()`,
|
||||
- `state.loaderData` for `useLoaderData()`
|
||||
- `state.errors` for `useRouteError()`
|
||||
- etc.
|
||||
|
||||
Enabling this flag means that you can now have fully-Transition-enabled navigations that play nicely with any other ongoing Transition-enabled aspects of your application.
|
||||
|
||||
The only APIs that are automatically wrapped in an async Transition are `<Link>` and `<Form>`. For everything else, you need to wrap the operation in `startTransition` yourself.
|
||||
|
||||
```tsx
|
||||
// Automatically Transition-enabled
|
||||
<Link to="/path" />
|
||||
<Form method="post" action="/path" />
|
||||
|
||||
// Manually Transition-enabled
|
||||
startTransition(() => navigate("/path"));
|
||||
startTransition(() => submit(data, { method: 'post', action: "/path" }));
|
||||
startTransition(() => fetcher.load("/path"));
|
||||
startTransition(() => fetcher.submit(data, { method: "post", action: "/path" }));
|
||||
|
||||
// Not Transition-enabled
|
||||
navigate("/path");
|
||||
submit(data, { method: 'post', action: "/path" });
|
||||
fetcher.load("/path");
|
||||
fetcher.submit(data, { method: "post", action: "/path" });
|
||||
```
|
||||
|
||||
**Important:** You must always `return` or `await` the `navigate` promise inside `startTransition` so that the Transition encompasses the full duration of the navigation. If you forget to `return` or `await` the promise, the Transition will end prematurely and things won't work as expected.
|
||||
|
||||
```tsx
|
||||
// ✅ Returned promise
|
||||
startTransition(() => navigate("/path"));
|
||||
startTransition(() => {
|
||||
setOptimistic(something);
|
||||
return navigate("/path"));
|
||||
});
|
||||
|
||||
// ✅ Awaited promise
|
||||
startTransition(async () => {
|
||||
setOptimistic(something);
|
||||
await navigate("/path"));
|
||||
});
|
||||
|
||||
// ❌ Non-returned promise
|
||||
startTransition(() => {
|
||||
setOptimistic(something);
|
||||
navigate("/path"));
|
||||
});
|
||||
|
||||
// ❌ Non-Awaited promise
|
||||
startTransition(async () => {
|
||||
setOptimistic(something);
|
||||
navigate("/path"));
|
||||
});
|
||||
```
|
||||
|
||||
#### `popstate` navigations
|
||||
|
||||
There is currently a bug with optimistic states and `popstate`. If you need to read the current route during a back navigation, which cannot complete synchronously (e.g. Suspends on uncached data), you can set the optimistic state before navigating back or defer the optimistic update in a timer or microtask.
|
||||
|
||||
[react-18]: https://react.dev/blog/2022/03/29/react-v18
|
||||
[concurrent]: https://react.dev/blog/2022/03/29/react-v18#what-is-concurrent-react
|
||||
[transitions]: https://react.dev/blog/2022/03/29/react-v18#new-feature-transitions
|
||||
[use-transition]: https://react.dev/reference/react/useTransition#reference
|
||||
[start-transition]: https://react.dev/reference/react/startTransition
|
||||
[react-19]: https://react.dev/blog/2024/12/05/react-19
|
||||
[actions]: https://react.dev/blog/2024/12/05/react-19#actions
|
||||
[use-optimistic-blog]: https://react.dev/blog/2024/12/05/react-19#new-hook-optimistic-updates
|
||||
[use-optimistic]: https://react.dev/reference/react/useOptimistic
|
||||
[flush-sync]: https://react.dev/reference/react-dom/flushSync
|
||||
[rr-6-13-0]: https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#v6130
|
||||
[uses-transition-issue]: https://github.com/facebook/react/issues/26382
|
||||
[uses-transition-tweet]: https://x.com/rickhanlonii/status/1683636856808775682
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Route Matching
|
||||
hidden: true
|
||||
# want to explain how the matching algorithm works with any potential gotchas
|
||||
---
|
||||
|
||||
# Route Matching
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: Server vs. Client Code Execution
|
||||
hidden: true
|
||||
---
|
||||
+465
@@ -0,0 +1,465 @@
|
||||
---
|
||||
title: Sessions and Cookies
|
||||
---
|
||||
|
||||
# Sessions and Cookies
|
||||
|
||||
[MODES: framework, data]
|
||||
|
||||
## Sessions
|
||||
|
||||
Sessions are an important part of websites that allow the server to identify requests coming from the same person, especially when it comes to server-side form validation or when JavaScript is not on the page. Sessions are a fundamental building block of many sites that let users "log in", including social, e-commerce, business, and educational websites.
|
||||
|
||||
When using React Router as your framework, sessions are managed on a per-route basis (rather than something like express middleware) in your `loader` and `action` methods using a "session storage" object (that implements the [`SessionStorage`][session-storage] interface). Session storage understands how to parse and generate cookies, and how to store session data in a database or filesystem.
|
||||
|
||||
### Using Sessions
|
||||
|
||||
This is an example of a cookie session storage:
|
||||
|
||||
```ts filename=app/sessions.server.ts
|
||||
import { createCookieSessionStorage } from "react-router";
|
||||
|
||||
type SessionData = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
type SessionFlashData = {
|
||||
error: string;
|
||||
};
|
||||
|
||||
const { getSession, commitSession, destroySession } =
|
||||
createCookieSessionStorage<SessionData, SessionFlashData>(
|
||||
{
|
||||
// a Cookie from `createCookie` or the CookieOptions to create one
|
||||
cookie: {
|
||||
name: "__session",
|
||||
|
||||
// all of these are optional
|
||||
domain: "reactrouter.com",
|
||||
// Expires can also be set (although maxAge overrides it when used in combination).
|
||||
// Note that this method is NOT recommended as `new Date` creates only one date on each server deployment, not a dynamic date in the future!
|
||||
//
|
||||
// expires: new Date(Date.now() + 60_000),
|
||||
httpOnly: true,
|
||||
maxAge: 60,
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
secrets: ["s3cret1"],
|
||||
secure: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export { getSession, commitSession, destroySession };
|
||||
```
|
||||
|
||||
We recommend setting up your session storage object in `app/sessions.server.ts` so all routes that need to access session data can import from the same spot.
|
||||
|
||||
The input/output to a session storage object are HTTP cookies. `getSession()` retrieves the current session from the incoming request's `Cookie` header, and `commitSession()`/`destroySession()` provide the `Set-Cookie` header for the outgoing response.
|
||||
|
||||
You'll use methods to get access to sessions in your `loader` and `action` functions.
|
||||
|
||||
After retrieving a session with `getSession`, the returned session object has a handful of methods and properties:
|
||||
|
||||
```tsx
|
||||
export async function action({
|
||||
request,
|
||||
}: ActionFunctionArgs) {
|
||||
const session = await getSession(
|
||||
request.headers.get("Cookie"),
|
||||
);
|
||||
session.get("foo");
|
||||
session.has("bar");
|
||||
// etc.
|
||||
}
|
||||
```
|
||||
|
||||
See the [Session API][session-api] for all methods available on the session object.
|
||||
|
||||
### Login form example
|
||||
|
||||
A login form might look something like this:
|
||||
|
||||
```tsx filename=app/routes/login.tsx lines=[4-7,12-14,16,22,25,33-35,46,51,56,61]
|
||||
import { data, redirect } from "react-router";
|
||||
import type { Route } from "./+types/login";
|
||||
|
||||
import {
|
||||
getSession,
|
||||
commitSession,
|
||||
} from "../sessions.server";
|
||||
|
||||
export async function loader({
|
||||
request,
|
||||
}: Route.LoaderArgs) {
|
||||
const session = await getSession(
|
||||
request.headers.get("Cookie"),
|
||||
);
|
||||
|
||||
if (session.has("userId")) {
|
||||
// Redirect to the home page if they are already signed in.
|
||||
return redirect("/");
|
||||
}
|
||||
|
||||
return data(
|
||||
{ error: session.get("error") },
|
||||
{
|
||||
headers: {
|
||||
"Set-Cookie": await commitSession(session),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function action({
|
||||
request,
|
||||
}: Route.ActionArgs) {
|
||||
const session = await getSession(
|
||||
request.headers.get("Cookie"),
|
||||
);
|
||||
const form = await request.formData();
|
||||
const username = form.get("username");
|
||||
const password = form.get("password");
|
||||
|
||||
const userId = await validateCredentials(
|
||||
username,
|
||||
password,
|
||||
);
|
||||
|
||||
if (userId == null) {
|
||||
session.flash("error", "Invalid username/password");
|
||||
|
||||
// Redirect back to the login page with errors.
|
||||
return redirect("/login", {
|
||||
headers: {
|
||||
"Set-Cookie": await commitSession(session),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
session.set("userId", userId);
|
||||
|
||||
// Login succeeded, send them to the home page.
|
||||
return redirect("/", {
|
||||
headers: {
|
||||
"Set-Cookie": await commitSession(session),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default function Login({
|
||||
loaderData,
|
||||
}: Route.ComponentProps) {
|
||||
const { error } = loaderData;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{error ? <div className="error">{error}</div> : null}
|
||||
<form method="POST">
|
||||
<div>
|
||||
<p>Please sign in</p>
|
||||
</div>
|
||||
<label>
|
||||
Username: <input type="text" name="username" />
|
||||
</label>
|
||||
<label>
|
||||
Password:{" "}
|
||||
<input type="password" name="password" />
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
And then a logout form might look something like this:
|
||||
|
||||
```tsx filename=app/routes/logout.tsx
|
||||
import {
|
||||
getSession,
|
||||
destroySession,
|
||||
} from "../sessions.server";
|
||||
import type { Route } from "./+types/logout";
|
||||
|
||||
export async function action({
|
||||
request,
|
||||
}: Route.ActionArgs) {
|
||||
const session = await getSession(
|
||||
request.headers.get("Cookie"),
|
||||
);
|
||||
return redirect("/login", {
|
||||
headers: {
|
||||
"Set-Cookie": await destroySession(session),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default function LogoutRoute() {
|
||||
return (
|
||||
<>
|
||||
<p>Are you sure you want to log out?</p>
|
||||
<Form method="post">
|
||||
<button>Logout</button>
|
||||
</Form>
|
||||
<Link to="/">Never mind</Link>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
<docs-warning>It's important that you logout (or perform any mutation for that matter) in an `action` and not a `loader`. Otherwise you open your users to [Cross-Site Request Forgery][csrf] attacks.</docs-warning>
|
||||
|
||||
### Session Gotchas
|
||||
|
||||
Because of nested routes, multiple loaders can be called to construct a single page. When using `session.flash()` or `session.unset()`, you need to be sure no other loaders in the request are going to want to read that, otherwise you'll get race conditions. Typically if you're using flash, you'll want to have a single loader read it, if another loader wants a flash message, use a different key for that loader.
|
||||
|
||||
### Creating custom session storage
|
||||
|
||||
React Router makes it easy to store sessions in your own database if needed. The [`createSessionStorage()`][create-session-storage] API requires a `cookie` (for options for creating a cookie, see [cookies][cookies]) and a set of create, read, update, and delete (CRUD) methods for managing the session data. The cookie is used to persist the session ID.
|
||||
|
||||
- `createData` will be called from `commitSession` on the initial session creation when no session ID exists in the cookie
|
||||
- `readData` will be called from `getSession` when a session ID exists in the cookie
|
||||
- `updateData` will be called from `commitSession` when a session ID already exists in the cookie
|
||||
- `deleteData` is called from `destroySession`
|
||||
|
||||
The following example shows how you could do this using a generic database client:
|
||||
|
||||
```ts
|
||||
import { createSessionStorage } from "react-router";
|
||||
|
||||
function createDatabaseSessionStorage({
|
||||
cookie,
|
||||
host,
|
||||
port,
|
||||
}) {
|
||||
// Configure your database client...
|
||||
const db = createDatabaseClient(host, port);
|
||||
|
||||
return createSessionStorage({
|
||||
cookie,
|
||||
async createData(data, expires) {
|
||||
// `expires` is a Date after which the data should be considered
|
||||
// invalid. You could use it to invalidate the data somehow or
|
||||
// automatically purge this record from your database.
|
||||
const id = await db.insert(data);
|
||||
return id;
|
||||
},
|
||||
async readData(id) {
|
||||
return (await db.select(id)) || null;
|
||||
},
|
||||
async updateData(id, data, expires) {
|
||||
await db.update(id, data);
|
||||
},
|
||||
async deleteData(id) {
|
||||
await db.delete(id);
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
And then you can use it like this:
|
||||
|
||||
```ts
|
||||
const { getSession, commitSession, destroySession } =
|
||||
createDatabaseSessionStorage({
|
||||
host: "localhost",
|
||||
port: 1234,
|
||||
cookie: {
|
||||
name: "__session",
|
||||
sameSite: "lax",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The `expires` argument to `createData` and `updateData` is the same `Date` at which the cookie itself expires and is no longer valid. You can use this information to automatically purge the session record from your database to save on space, or to ensure that you do not otherwise return any data for old, expired cookies.
|
||||
|
||||
### Additional session utils
|
||||
|
||||
There are also several other session utilities available if you need them:
|
||||
|
||||
- [`isSession`][is-session]
|
||||
- [`createMemorySessionStorage`][create-memory-session-storage]
|
||||
- [`createSession`][create-session] (custom storage)
|
||||
- [`createFileSessionStorage`][create-file-session-storage] (node)
|
||||
- [`createWorkersKVSessionStorage`][create-workers-kv-session-storage] (Cloudflare Workers)
|
||||
- [`createArcTableSessionStorage`][create-arc-table-session-storage] (architect, Amazon DynamoDB)
|
||||
|
||||
## Cookies
|
||||
|
||||
A [cookie][cookie] is a small piece of information that your server sends someone in a HTTP response that their browser will send back on subsequent requests. This technique is a fundamental building block of many interactive websites that adds state so you can build authentication (see [sessions][sessions]), shopping carts, user preferences, and many other features that require remembering who is "logged in".
|
||||
|
||||
React Router's [`Cookie` interface][cookie-api] provides a logical, reusable container for cookie metadata.
|
||||
|
||||
### Using cookies
|
||||
|
||||
While you may create these cookies manually, it is more common to use a [session storage][sessions].
|
||||
|
||||
In React Router, you will typically work with cookies in your `loader` and/or `action` functions, since those are the places where you need to read and write data.
|
||||
|
||||
Let's say you have a banner on your e-commerce site that prompts users to check out the items you currently have on sale. The banner spans the top of your homepage, and includes a button on the side that allows the user to dismiss the banner so they don't see it for at least another week.
|
||||
|
||||
First, create a cookie:
|
||||
|
||||
```ts filename=app/cookies.server.ts
|
||||
import { createCookie } from "react-router";
|
||||
|
||||
export const userPrefs = createCookie("user-prefs", {
|
||||
maxAge: 604_800, // one week
|
||||
});
|
||||
```
|
||||
|
||||
Then, you can `import` the cookie and use it in your `loader` and/or `action`. The `loader` in this case just checks the value of the user preference so you can use it in your component for deciding whether to render the banner. When the button is clicked, the `<form>` calls the `action` on the server and reloads the page without the banner.
|
||||
|
||||
### User preferences example
|
||||
|
||||
```tsx filename=app/routes/home.tsx lines=[4,9-11,18-20,29]
|
||||
import { Link, Form, redirect } from "react-router";
|
||||
import type { Route } from "./+types/home";
|
||||
|
||||
import { userPrefs } from "../cookies.server";
|
||||
|
||||
export async function loader({
|
||||
request,
|
||||
}: Route.LoaderArgs) {
|
||||
const cookieHeader = request.headers.get("Cookie");
|
||||
const cookie =
|
||||
(await userPrefs.parse(cookieHeader)) || {};
|
||||
return { showBanner: cookie.showBanner };
|
||||
}
|
||||
|
||||
export async function action({
|
||||
request,
|
||||
}: Route.ActionArgs) {
|
||||
const cookieHeader = request.headers.get("Cookie");
|
||||
const cookie =
|
||||
(await userPrefs.parse(cookieHeader)) || {};
|
||||
const bodyParams = await request.formData();
|
||||
|
||||
if (bodyParams.get("bannerVisibility") === "hidden") {
|
||||
cookie.showBanner = false;
|
||||
}
|
||||
|
||||
return redirect("/", {
|
||||
headers: {
|
||||
"Set-Cookie": await userPrefs.serialize(cookie),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default function Home({
|
||||
loaderData,
|
||||
}: Route.ComponentProps) {
|
||||
return (
|
||||
<div>
|
||||
{loaderData.showBanner ? (
|
||||
<div>
|
||||
<Link to="/sale">Don't miss our sale!</Link>
|
||||
<Form method="post">
|
||||
<input
|
||||
type="hidden"
|
||||
name="bannerVisibility"
|
||||
value="hidden"
|
||||
/>
|
||||
<button type="submit">Hide</button>
|
||||
</Form>
|
||||
</div>
|
||||
) : null}
|
||||
<h1>Welcome!</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Cookie attributes
|
||||
|
||||
Cookies have [several attributes][cookie-attrs] that control when they expire, how they are accessed, and where they are sent. Any of these attributes may be specified either in `createCookie(name, options)`, or during `serialize()` when the `Set-Cookie` header is generated.
|
||||
|
||||
```ts
|
||||
const cookie = createCookie("user-prefs", {
|
||||
// These are defaults for this cookie.
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
expires: new Date(Date.now() + 60_000),
|
||||
maxAge: 60,
|
||||
});
|
||||
|
||||
// You can either use the defaults:
|
||||
cookie.serialize(userPrefs);
|
||||
|
||||
// Or override individual ones as needed:
|
||||
cookie.serialize(userPrefs, { sameSite: "strict" });
|
||||
```
|
||||
|
||||
Please read [more info about these attributes][cookie-attrs] to get a better understanding of what they do.
|
||||
|
||||
### Signing cookies
|
||||
|
||||
It is possible to sign a cookie to automatically verify its contents when it is received. Since it's relatively easy to spoof HTTP headers, this is a good idea for any information that you do not want someone to be able to fake, like authentication information (see [sessions][sessions]).
|
||||
|
||||
To sign a cookie, provide one or more `secrets` when you first create the cookie:
|
||||
|
||||
```ts
|
||||
const cookie = createCookie("user-prefs", {
|
||||
secrets: ["s3cret1"],
|
||||
});
|
||||
```
|
||||
|
||||
Cookies that have one or more `secrets` will be stored and verified in a way that ensures the cookie's integrity.
|
||||
|
||||
Secrets may be rotated by adding new secrets to the front of the `secrets` array. Cookies that have been signed with old secrets will still be decoded successfully in `cookie.parse()`, and the newest secret (the first one in the array) will always be used to sign outgoing cookies created in `cookie.serialize()`.
|
||||
|
||||
```ts filename=app/cookies.server.ts
|
||||
export const cookie = createCookie("user-prefs", {
|
||||
secrets: ["n3wsecr3t", "olds3cret"],
|
||||
});
|
||||
```
|
||||
|
||||
```tsx filename=app/routes/my-route.tsx
|
||||
import { data } from "react-router";
|
||||
import { cookie } from "../cookies.server";
|
||||
import type { Route } from "./+types/my-route";
|
||||
|
||||
export async function loader({
|
||||
request,
|
||||
}: Route.LoaderArgs) {
|
||||
const oldCookie = request.headers.get("Cookie");
|
||||
// oldCookie may have been signed with "olds3cret", but still parses ok
|
||||
const value = await cookie.parse(oldCookie);
|
||||
|
||||
return data("...", {
|
||||
headers: {
|
||||
// Set-Cookie is signed with "n3wsecr3t"
|
||||
"Set-Cookie": await cookie.serialize(value),
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Additional cookie utils
|
||||
|
||||
There are also several other cookie utilities available if you need them:
|
||||
|
||||
- [`isCookie`][is-cookie]
|
||||
- [`createCookie`][create-cookie]
|
||||
|
||||
To learn more about each attribute, please see the [MDN Set-Cookie docs][cookie-attrs].
|
||||
|
||||
[csrf]: https://developer.mozilla.org/en-US/docs/Glossary/CSRF
|
||||
[cookies]: #cookies
|
||||
[sessions]: #sessions
|
||||
[session-storage]: https://api.reactrouter.com/v7/interfaces/react-router.SessionStorage
|
||||
[session-api]: https://api.reactrouter.com/v7/interfaces/react-router.Session
|
||||
[is-session]: https://api.reactrouter.com/v7/functions/react-router.isSession
|
||||
[cookie-api]: https://api.reactrouter.com/v7/interfaces/react-router.Cookie
|
||||
[create-session-storage]: https://api.reactrouter.com/v7/functions/react-router.createSessionStorage
|
||||
[create-session]: https://api.reactrouter.com/v7/functions/react-router.createSession
|
||||
[create-memory-session-storage]: https://api.reactrouter.com/v7/functions/react-router.createMemorySessionStorage
|
||||
[create-file-session-storage]: https://api.reactrouter.com/v7/functions/_react-router_node.createFileSessionStorage
|
||||
[create-workers-kv-session-storage]: https://api.reactrouter.com/v7/functions/_react-router_cloudflare.createWorkersKVSessionStorage
|
||||
[create-arc-table-session-storage]: https://api.reactrouter.com/v7/functions/_react-router_architect.createArcTableSessionStorage
|
||||
[cookie]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies
|
||||
[cookie-attrs]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes
|
||||
[is-cookie]: https://api.reactrouter.com/v7/functions/react-router.isCookie
|
||||
[create-cookie]: https://api.reactrouter.com/v7/functions/react-router.createCookie
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
---
|
||||
title: Special Files
|
||||
hidden: true
|
||||
---
|
||||
|
||||
# Special Files
|
||||
|
||||
The content of this page has been moved to the following:
|
||||
|
||||
- [`react-router.config.ts`](../api/framework-conventions/react-router.config.ts) - Optional configuration file for your app
|
||||
- [`root.tsx`](../api/framework-conventions/root.tsx) - Required root route that renders the HTML document
|
||||
- [`routes.ts`](../api/framework-conventions/routes.ts) - Required route configuration mapping URLs to components
|
||||
- [`entry.client.tsx`](../api/framework-conventions/entry.client.tsx) - Optional client-side entry point for hydration
|
||||
- [`entry.server.tsx`](../api/framework-conventions/entry.server.tsx) - Optional server-side entry point for rendering
|
||||
- [`.server` modules](../api/framework-conventions/server-modules) - Server-only modules excluded from client bundles
|
||||
- [`.client` modules](../api/framework-conventions/client-modules) - Client-only modules excluded from server bundles
|
||||
+524
@@ -0,0 +1,524 @@
|
||||
---
|
||||
title: State Management
|
||||
---
|
||||
|
||||
# State Management
|
||||
|
||||
[MODES: framework, data]
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
State management in React typically involves maintaining a synchronized cache of server data on the client side. However, when using React Router as your framework, most of the traditional caching solutions become redundant because of how it inherently handles data synchronization.
|
||||
|
||||
## Understanding State Management in React
|
||||
|
||||
In a typical React context, when we refer to "state management", we're primarily discussing how we synchronize server state with the client. A more apt term could be "cache management" because the server is the source of truth and the client state is mostly functioning as a cache.
|
||||
|
||||
Popular caching solutions in React include:
|
||||
|
||||
- **[Redux][redux]:** A predictable state container for JavaScript apps.
|
||||
- **[TanStack Query][tanstack_query]:** Hooks for fetching, caching, and updating asynchronous data in React.
|
||||
- **[Apollo][apollo]:** A comprehensive state management library for JavaScript that integrates with GraphQL.
|
||||
|
||||
In certain scenarios, using these libraries may be warranted. However, with React Router's unique server-focused approach, their utility becomes less prevalent. In fact, most React Router applications forgo them entirely.
|
||||
|
||||
## How React Router Simplifies State
|
||||
|
||||
React Router seamlessly bridges the gap between the backend and frontend via mechanisms like loaders, actions, and forms with automatic synchronization through revalidation. This offers developers the ability to directly use server state within components without managing a cache, the network communication, or data revalidation, making most client-side caching redundant.
|
||||
|
||||
Here's why using typical React state patterns might be an anti-pattern in React Router:
|
||||
|
||||
1. **Network-related State:** If your React state is managing anything related to the network—such as data from loaders, pending form submissions, or navigational states—it's likely that you're managing state that React Router already manages:
|
||||
- **[`useNavigation`][use_navigation]**: This hook gives you access to `navigation.state`, `navigation.formData`, `navigation.location`, etc.
|
||||
- **[`useFetcher`][use_fetcher]**: This facilitates interaction with `fetcher.state`, `fetcher.formData`, `fetcher.data` etc.
|
||||
- **[`loaderData`][loader_data]**: Access the data for a route.
|
||||
- **[`actionData`][action_data]**: Access the data from the latest action.
|
||||
|
||||
2. **Storing Data in React Router:** A lot of data that developers might be tempted to store in React state has a more natural home in React Router, such as:
|
||||
- **URL Search Params:** Parameters within the URL that hold state.
|
||||
- **[Cookies][cookies]:** Small pieces of data stored on the user's device.
|
||||
- **[Server Sessions][sessions]:** Server-managed user sessions.
|
||||
- **Server Caches:** Cached data on the server side for quicker retrieval.
|
||||
|
||||
3. **Performance Considerations:** At times, client state is leveraged to avoid redundant data fetching. With React Router, you can use the [`Cache-Control`][cache_control_header] headers within `loader`s, allowing you to tap into the browser's native cache. However, this approach has its limitations and should be used judiciously. It's usually more beneficial to optimize backend queries or implement a server cache. This is because such changes benefit all users and do away with the need for individual browser caches.
|
||||
|
||||
As a developer transitioning to React Router, it's essential to recognize and embrace its inherent efficiencies rather than applying traditional React patterns. React Router offers a streamlined solution to state management leading to less code, fresh data, and no state synchronization bugs.
|
||||
|
||||
## Examples
|
||||
|
||||
### Network Related State
|
||||
|
||||
For examples on using React Router's internal state to manage network related state, refer to [Pending UI][pending_ui].
|
||||
|
||||
### URL Search Params
|
||||
|
||||
Consider a UI that lets the user customize between list view or detail view. Your instinct might be to reach for React state:
|
||||
|
||||
```tsx bad lines=[2,6,9]
|
||||
export function List() {
|
||||
const [view, setView] = useState("list");
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<button onClick={() => setView("list")}>
|
||||
View as List
|
||||
</button>
|
||||
<button onClick={() => setView("details")}>
|
||||
View with Details
|
||||
</button>
|
||||
</div>
|
||||
{view === "list" ? <ListView /> : <DetailView />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Now consider you want the URL to update when the user changes the view. Note the state synchronization:
|
||||
|
||||
```tsx bad lines=[7,16,24]
|
||||
import { useNavigate, useSearchParams } from "react-router";
|
||||
|
||||
export function List() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [view, setView] = useState(
|
||||
searchParams.get("view") || "list",
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setView("list");
|
||||
navigate(`?view=list`);
|
||||
}}
|
||||
>
|
||||
View as List
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setView("details");
|
||||
navigate(`?view=details`);
|
||||
}}
|
||||
>
|
||||
View with Details
|
||||
</button>
|
||||
</div>
|
||||
{view === "list" ? <ListView /> : <DetailView />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Instead of synchronizing state, you can simply read and set the state in the URL directly with boring old HTML forms:
|
||||
|
||||
```tsx good lines=[5,9-16]
|
||||
import { Form, useSearchParams } from "react-router";
|
||||
|
||||
export function List() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const view = searchParams.get("view") || "list";
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form>
|
||||
<button name="view" value="list">
|
||||
View as List
|
||||
</button>
|
||||
<button name="view" value="details">
|
||||
View with Details
|
||||
</button>
|
||||
</Form>
|
||||
{view === "list" ? <ListView /> : <DetailView />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Persistent UI State
|
||||
|
||||
Consider a UI that toggles a sidebar's visibility. We have three ways to handle the state:
|
||||
|
||||
1. React state
|
||||
2. Browser local storage
|
||||
3. Cookies
|
||||
|
||||
In this discussion, we'll break down the trade-offs associated with each method.
|
||||
|
||||
#### React State
|
||||
|
||||
React state provides a simple solution for temporary state storage.
|
||||
|
||||
**Pros**:
|
||||
|
||||
- **Simple**: Easy to implement and understand.
|
||||
- **Encapsulated**: State is scoped to the component.
|
||||
|
||||
**Cons**:
|
||||
|
||||
- **Transient**: Doesn't survive page refreshes, returning to the page later, or unmounting and remounting the component.
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```tsx
|
||||
function Sidebar() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => setIsOpen((open) => !open)}>
|
||||
{isOpen ? "Close" : "Open"}
|
||||
</button>
|
||||
<aside hidden={!isOpen}>
|
||||
<Outlet />
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Local Storage
|
||||
|
||||
To persist state beyond the component lifecycle, browser local storage is a step-up. See our doc on [Client Data][client_data] for more advanced examples.
|
||||
|
||||
**Pros**:
|
||||
|
||||
- **Persistent**: Maintains state across page refreshes and component mounts/unmounts.
|
||||
- **Encapsulated**: State is scoped to the component.
|
||||
|
||||
**Cons**:
|
||||
|
||||
- **Requires Synchronization**: React components must sync up with local storage to initialize and save the current state.
|
||||
- **Server Rendering Limitation**: The [`window`][window_global] and [`localStorage`][local_storage_global] objects are not accessible during server-side rendering, so state must be initialized in the browser with an effect.
|
||||
- **UI Flickering**: On initial page loads, the state in local storage may not match what was rendered by the server and the UI will flicker when JavaScript loads.
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```tsx
|
||||
function Sidebar() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
// synchronize initially
|
||||
useLayoutEffect(() => {
|
||||
const isOpen = window.localStorage.getItem("sidebar");
|
||||
setIsOpen(isOpen);
|
||||
}, []);
|
||||
|
||||
// synchronize on change
|
||||
useEffect(() => {
|
||||
window.localStorage.setItem("sidebar", isOpen);
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => setIsOpen((open) => !open)}>
|
||||
{isOpen ? "Close" : "Open"}
|
||||
</button>
|
||||
<aside hidden={!isOpen}>
|
||||
<Outlet />
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
In this approach, state must be initialized within an effect. This is crucial to avoid complications during server-side rendering. Directly initializing the React state from `localStorage` will cause errors since `window.localStorage` is unavailable during server rendering.
|
||||
|
||||
```tsx bad lines=[4]
|
||||
function Sidebar() {
|
||||
const [isOpen, setIsOpen] = useState(
|
||||
// error: window is not defined
|
||||
window.localStorage.getItem("sidebar"),
|
||||
);
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
By initializing the state within an effect, there's potential for a mismatch between the server-rendered state and the state stored in local storage. This discrepancy will lead to brief UI flickering shortly after the page renders and should be avoided.
|
||||
|
||||
#### Cookies
|
||||
|
||||
Cookies offer a comprehensive solution for this use case. However, this method introduces added preliminary setup before making the state accessible within the component.
|
||||
|
||||
**Pros**:
|
||||
|
||||
- **Server Rendering**: State is available on the server for rendering and even for server actions.
|
||||
- **Single Source of Truth**: Eliminates state synchronization hassles.
|
||||
- **Persistence**: Maintains state across page loads and component mounts/unmounts. State can even persist across devices if you switch to a database-backed session.
|
||||
- **Progressive Enhancement**: Functions even before JavaScript loads.
|
||||
|
||||
**Cons**:
|
||||
|
||||
- **Boilerplate**: Requires more code because of the network.
|
||||
- **Exposed**: The state is not encapsulated to a single component, other parts of the app must be aware of the cookie.
|
||||
|
||||
**Implementation**:
|
||||
|
||||
First we'll need to create a cookie object:
|
||||
|
||||
```tsx
|
||||
import { createCookie } from "react-router";
|
||||
export const prefs = createCookie("prefs");
|
||||
```
|
||||
|
||||
Next we set up the server action and loader to read and write the cookie:
|
||||
|
||||
```tsx filename=app/routes/sidebar.tsx
|
||||
import { data, Outlet } from "react-router";
|
||||
import type { Route } from "./+types/sidebar";
|
||||
|
||||
import { prefs } from "./prefs-cookie";
|
||||
|
||||
// read the state from the cookie
|
||||
export async function loader({
|
||||
request,
|
||||
}: Route.LoaderArgs) {
|
||||
const cookieHeader = request.headers.get("Cookie");
|
||||
const cookie = (await prefs.parse(cookieHeader)) || {};
|
||||
return data({ sidebarIsOpen: cookie.sidebarIsOpen });
|
||||
}
|
||||
|
||||
// write the state to the cookie
|
||||
export async function action({
|
||||
request,
|
||||
}: Route.ActionArgs) {
|
||||
const cookieHeader = request.headers.get("Cookie");
|
||||
const cookie = (await prefs.parse(cookieHeader)) || {};
|
||||
const formData = await request.formData();
|
||||
|
||||
const isOpen = formData.get("sidebar") === "open";
|
||||
cookie.sidebarIsOpen = isOpen;
|
||||
|
||||
return data(isOpen, {
|
||||
headers: {
|
||||
"Set-Cookie": await prefs.serialize(cookie),
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
After the server code is set up, we can use the cookie state in our UI:
|
||||
|
||||
```tsx
|
||||
function Sidebar({ loaderData }: Route.ComponentProps) {
|
||||
const fetcher = useFetcher();
|
||||
let { sidebarIsOpen } = loaderData;
|
||||
|
||||
// use optimistic UI to immediately change the UI state
|
||||
if (fetcher.formData?.has("sidebar")) {
|
||||
sidebarIsOpen =
|
||||
fetcher.formData.get("sidebar") === "open";
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<fetcher.Form method="post">
|
||||
<button
|
||||
name="sidebar"
|
||||
value={sidebarIsOpen ? "closed" : "open"}
|
||||
>
|
||||
{sidebarIsOpen ? "Close" : "Open"}
|
||||
</button>
|
||||
</fetcher.Form>
|
||||
<aside hidden={!sidebarIsOpen}>
|
||||
<Outlet />
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
While this is certainly more code that touches more of the application to account for the network requests and responses, the UX is greatly improved. Additionally, state comes from a single source of truth without any state synchronization required.
|
||||
|
||||
In summary, each of the discussed methods offers a unique set of benefits and challenges:
|
||||
|
||||
- **React state**: Offers simple but transient state management.
|
||||
- **Local Storage**: Provides persistence but with synchronization requirements and UI flickering.
|
||||
- **Cookies**: Delivers robust, persistent state management at the cost of added boilerplate.
|
||||
|
||||
None of these are wrong, but if you want to persist the state across visits, cookies offer the best user experience.
|
||||
|
||||
### Form Validation and Action Data
|
||||
|
||||
Client-side validation can augment the user experience, but similar enhancements can be achieved by leaning more towards server-side processing and letting it handle the complexities.
|
||||
|
||||
The following example illustrates the inherent complexities of managing network state, coordinating state from the server, and implementing validation redundantly on both the client and server sides. It's just for illustration, so forgive any obvious bugs or problems you find.
|
||||
|
||||
```tsx bad lines=[2,11,27,38,63]
|
||||
export function Signup() {
|
||||
// A multitude of React State declarations
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const [userName, setUserName] = useState("");
|
||||
const [userNameError, setUserNameError] = useState(null);
|
||||
|
||||
const [password, setPassword] = useState(null);
|
||||
const [passwordError, setPasswordError] = useState("");
|
||||
|
||||
// Replicating server-side logic in the client
|
||||
function validateForm() {
|
||||
setUserNameError(null);
|
||||
setPasswordError(null);
|
||||
const errors = validateSignupForm(userName, password);
|
||||
if (errors) {
|
||||
if (errors.userName) {
|
||||
setUserNameError(errors.userName);
|
||||
}
|
||||
if (errors.password) {
|
||||
setPasswordError(errors.password);
|
||||
}
|
||||
}
|
||||
return Boolean(errors);
|
||||
}
|
||||
|
||||
// Manual network interaction handling
|
||||
async function handleSubmit() {
|
||||
if (validateForm()) {
|
||||
setSubmitting(true);
|
||||
const res = await postJSON("/api/signup", {
|
||||
userName,
|
||||
password,
|
||||
});
|
||||
const json = await res.json();
|
||||
setIsSubmitting(false);
|
||||
|
||||
// Server state synchronization to the client
|
||||
if (json.errors) {
|
||||
if (json.errors.userName) {
|
||||
setUserNameError(json.errors.userName);
|
||||
}
|
||||
if (json.errors.password) {
|
||||
setPasswordError(json.errors.password);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
handleSubmit();
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
value={userName}
|
||||
onChange={() => {
|
||||
// Synchronizing form state for the fetch
|
||||
setUserName(event.target.value);
|
||||
}}
|
||||
/>
|
||||
{userNameError ? <i>{userNameError}</i> : null}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
onChange={(event) => {
|
||||
// Synchronizing form state for the fetch
|
||||
setPassword(event.target.value);
|
||||
}}
|
||||
/>
|
||||
{passwordError ? <i>{passwordError}</i> : null}
|
||||
</p>
|
||||
|
||||
<button disabled={isSubmitting} type="submit">
|
||||
Sign Up
|
||||
</button>
|
||||
|
||||
{isSubmitting ? <BusyIndicator /> : null}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
The backend endpoint, `/api/signup`, also performs validation and sends error feedback. Note that some essential validation, like detecting duplicate usernames, can only be done server-side using information the client doesn't have access to.
|
||||
|
||||
```tsx bad
|
||||
export async function signupHandler(request: Request) {
|
||||
const errors = await validateSignupRequest(request);
|
||||
if (errors) {
|
||||
return { ok: false, errors: errors };
|
||||
}
|
||||
await signupUser(request);
|
||||
return { ok: true, errors: null };
|
||||
}
|
||||
```
|
||||
|
||||
Now, let's contrast this with a React Router-based implementation. The action remains consistent, but the component is vastly simplified due to the direct utilization of server state via `actionData`, and leveraging the network state that React Router inherently manages.
|
||||
|
||||
```tsx filename=app/routes/signup.tsx good lines=[20-22]
|
||||
import { useNavigation } from "react-router";
|
||||
import type { Route } from "./+types/signup";
|
||||
|
||||
export async function action({
|
||||
request,
|
||||
}: ActionFunctionArgs) {
|
||||
const errors = await validateSignupRequest(request);
|
||||
if (errors) {
|
||||
return { ok: false, errors: errors };
|
||||
}
|
||||
await signupUser(request);
|
||||
return { ok: true, errors: null };
|
||||
}
|
||||
|
||||
export function Signup({
|
||||
actionData,
|
||||
}: Route.ComponentProps) {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const userNameError = actionData?.errors?.userName;
|
||||
const passwordError = actionData?.errors?.password;
|
||||
const isSubmitting = navigation.formAction === "/signup";
|
||||
|
||||
return (
|
||||
<Form method="post">
|
||||
<p>
|
||||
<input type="text" name="username" />
|
||||
{userNameError ? <i>{userNameError}</i> : null}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input type="password" name="password" />
|
||||
{passwordError ? <i>{passwordError}</i> : null}
|
||||
</p>
|
||||
|
||||
<button disabled={isSubmitting} type="submit">
|
||||
Sign Up
|
||||
</button>
|
||||
|
||||
{isSubmitting ? <BusyIndicator /> : null}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
The extensive state management from our previous example is distilled into just three code lines. We eliminate the necessity for React state, change event listeners, submit handlers, and state management libraries for such network interactions.
|
||||
|
||||
Direct access to the server state is made possible through `actionData`, and network state through `useNavigation` (or `useFetcher`).
|
||||
|
||||
As bonus party trick, the form is functional even before JavaScript loads (see [Progressive Enhancement][progressive_enhancement]). Instead of React Router managing the network operations, the default browser behaviors step in.
|
||||
|
||||
If you ever find yourself entangled in managing and synchronizing state for network operations, React Router likely offers a more elegant solution.
|
||||
|
||||
[redux]: https://redux.js.org/
|
||||
[tanstack_query]: https://tanstack.com/query/latest
|
||||
[apollo]: https://www.apollographql.com/
|
||||
[use_navigation]: https://api.reactrouter.com/v7/functions/react-router.useNavigation
|
||||
[use_fetcher]: https://api.reactrouter.com/v7/functions/react-router.useFetcher
|
||||
[loader_data]: ../start/framework/data-loading
|
||||
[action_data]: ../start/framework/actions
|
||||
[cookies]: ./sessions-and-cookies#cookies
|
||||
[sessions]: ./sessions-and-cookies#sessions
|
||||
[cache_control_header]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
|
||||
[pending_ui]: ../start/framework/pending-ui
|
||||
[client_data]: ../how-to/client-data
|
||||
[window_global]: https://developer.mozilla.org/en-US/docs/Web/API/Window/window
|
||||
[local_storage_global]: https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
|
||||
[progressive_enhancement]: ./progressive-enhancement
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: Styling
|
||||
---
|
||||
|
||||
# Styling
|
||||
|
||||
[MODES: framework]
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
Framework mode uses the React Router Vite plugin, so the styling story is mostly just Vite's styling story.
|
||||
|
||||
React Router does not have a separate CSS pipeline for Framework mode. In practice, there are three patterns that matter:
|
||||
|
||||
1. Import CSS as a side effect
|
||||
2. Use the route module `links` export
|
||||
3. Render a stylesheet `<link>` directly
|
||||
|
||||
## Side-Effect CSS Imports
|
||||
|
||||
Because Framework mode uses Vite, you can import CSS files as side effects:
|
||||
|
||||
```tsx filename=app/root.tsx
|
||||
import "./app.css";
|
||||
```
|
||||
|
||||
```tsx filename=app/routes/dashboard.tsx
|
||||
import "./dashboard.css";
|
||||
```
|
||||
|
||||
This is often the simplest option. Global styles can be imported in `root.tsx`, and route or component styles can be imported next to the module that uses them.
|
||||
|
||||
## `links` Export
|
||||
|
||||
React Router also supports adding stylesheets through the route module `links` export.
|
||||
|
||||
This is useful when you want a stylesheet URL from Vite and need React Router to render a real `<link rel="stylesheet">` tag for the route:
|
||||
|
||||
```tsx filename=app/routes/dashboard.tsx
|
||||
import dashboardHref from "./dashboard.css?url";
|
||||
|
||||
export function links() {
|
||||
return [{ rel: "stylesheet", href: dashboardHref }];
|
||||
}
|
||||
```
|
||||
|
||||
The `links` export feeds the [`<Links />`][links-component] component in your root route. This is the React Router-specific styling API in Framework mode. For more on route module exports, see [Route Module][route-module].
|
||||
|
||||
## Direct `<link>` Rendering
|
||||
|
||||
If you're using React 19, you can also render a stylesheet `<link>` directly in your route component:
|
||||
|
||||
```tsx filename=app/routes/dashboard.tsx
|
||||
import dashboardHref from "./dashboard.css?url";
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href={dashboardHref}
|
||||
precedence="default"
|
||||
/>
|
||||
<h1>Dashboard</h1>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
This uses React's built-in [`<link>`][react-link] support, which hoists the stylesheet into the document `<head>`. That gives you another way to colocate stylesheet tags with the route that needs them.
|
||||
|
||||
## Everything Else
|
||||
|
||||
For CSS Modules, Tailwind, PostCSS, Sass, Vanilla Extract, and other styling tools, use the normal Vite setup for those tools.
|
||||
|
||||
See:
|
||||
|
||||
- [Vite CSS Features][vite-css]
|
||||
- [Vite Static Asset Handling][vite-assets]
|
||||
- [`<Links />`][links-component]
|
||||
|
||||
[links-component]: ../api/components/Links
|
||||
[react-link]: https://react.dev/reference/react-dom/components/link
|
||||
[route-module]: ../start/framework/route-module
|
||||
[vite-assets]: https://vite.dev/guide/assets.html
|
||||
[vite-css]: https://vite.dev/guide/features.html#css
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: Type Safety
|
||||
---
|
||||
|
||||
# Type Safety
|
||||
|
||||
[MODES: framework]
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
If you haven't done so already, check out our guide for [setting up type safety][route-module-type-safety] in a new project.
|
||||
|
||||
React Router generates types for each route in your app to provide type safety for the route module exports.
|
||||
|
||||
For example, let's say you have a `products/:id` route configured:
|
||||
|
||||
```ts filename=app/routes.ts
|
||||
import {
|
||||
type RouteConfig,
|
||||
route,
|
||||
} from "@react-router/dev/routes";
|
||||
|
||||
export default [
|
||||
route("products/:id", "./routes/product.tsx"),
|
||||
] satisfies RouteConfig;
|
||||
```
|
||||
|
||||
You can import route-specific types like so:
|
||||
|
||||
```tsx filename=app/routes/product.tsx
|
||||
import type { Route } from "./+types/product";
|
||||
// types generated for this route 👆
|
||||
|
||||
export function loader({ params }: Route.LoaderArgs) {
|
||||
// 👆 { id: string }
|
||||
return { planet: `world #${params.id}` };
|
||||
}
|
||||
|
||||
export default function Component({
|
||||
loaderData, // 👈 { planet: string }
|
||||
}: Route.ComponentProps) {
|
||||
return <h1>Hello, {loaderData.planet}!</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
React Router's type generation executes your route config (`app/routes.ts` by default) to determine the routes for your app.
|
||||
It then generates a `+types/<route file>.d.ts` for each route within a special `.react-router/types/` directory.
|
||||
With [`rootDirs` configured][route-module-type-safety], TypeScript can import these generated files as if they were right next to their corresponding route modules.
|
||||
|
||||
For a deeper dive into some of the design decisions, check out our [type inference decision doc](https://github.com/remix-run/react-router/blob/main/decisions/0012-type-inference.md).
|
||||
|
||||
[route-module-type-safety]: ../how-to/route-module-type-safety
|
||||
|
||||
## `typegen` command
|
||||
|
||||
You can manually generate types with the `typegen` command:
|
||||
|
||||
```sh
|
||||
react-router typegen
|
||||
```
|
||||
|
||||
The following types are generated for each route:
|
||||
|
||||
- `LoaderArgs`
|
||||
- `ClientLoaderArgs`
|
||||
- `ActionArgs`
|
||||
- `ClientActionArgs`
|
||||
- `HydrateFallbackProps`
|
||||
- `ComponentProps` (for the `default` export)
|
||||
- `ErrorBoundaryProps`
|
||||
|
||||
### --watch
|
||||
|
||||
If you run `react-router dev` — or if your custom server calls `vite.createServer` — then React Router's Vite plugin is already generating up-to-date types for you.
|
||||
But if you really need to run type generation on its own, you can also use `--watch` to automatically regenerate types as files change:
|
||||
|
||||
```sh
|
||||
react-router typegen --watch
|
||||
```
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: Accessibility
|
||||
---
|
||||
|
||||
# Accessibility
|
||||
|
||||
Accessibility in a React Router app looks a lot like accessibility on the web in general. Using proper semantic markup and following the [Web Content Accessibility Guidelines (WCAG)][wcag] will get you most of the way there.
|
||||
|
||||
React Router makes certain accessibility practices the default where possible and provides APIs to help where it's not.
|
||||
|
||||
## Links
|
||||
|
||||
[MODES: framework, data, declarative]
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
The [`<Link>` component][link] renders a standard anchor tag, meaning that you get its accessibility behaviors from the browser for free!
|
||||
|
||||
React Router also provides the [`<NavLink/>`][navlink] which behaves the same as `<Link>`, but it also provides context for assistive technology when the link points to the current page. This is useful for building navigation menus or breadcrumbs.
|
||||
|
||||
## Routing
|
||||
|
||||
[MODES: framework]
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
If you are rendering [`<Scripts>`][scripts] in your app, there are some important things to consider to make client-side routing more accessible for your users.
|
||||
|
||||
With a traditional multi-page website we don't have to think about route changes too much. Your app renders an anchor tag, and the browser handles the rest. If your users disable JavaScript, your React Router app should already work this way by default!
|
||||
|
||||
When the client scripts in React Router are loaded, React Router takes control of routing and prevents the browser's default behavior. React Router doesn't make any assumptions about your UI as the route changes. There are some important features you'll want to consider as a result, including:
|
||||
|
||||
- **Focus management:** What element receives focus when the route changes? This is important for keyboard users and can be helpful for screen-reader users.
|
||||
- **Live-region announcements:** Screen-reader users also benefit from announcements when a route has changed. You may want to also notify them during certain transition states depending on the nature of the change and how long loading is expected to take.
|
||||
|
||||
In 2019, [Marcy Sutton led and published findings from user research][marcy-sutton-led-and-published-findings-from-user-research] to help developers build accessible client-side routing experiences.
|
||||
|
||||
[link]: ../api/components/Link
|
||||
[navlink]: ../api/components/NavLink
|
||||
[scripts]: ../api/components/Scripts
|
||||
[wcag]: https://www.w3.org/WAI/standards-guidelines/wcag/
|
||||
[marcy-sutton-led-and-published-findings-from-user-research]: https://www.gatsbyjs.com/blog/2019-07-11-user-testing-accessible-client-routing
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
---
|
||||
title: Client Data
|
||||
---
|
||||
|
||||
# Client Data
|
||||
|
||||
[MODES: framework]
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
You can fetch and mutate data directly in the browser using `clientLoader` and `clientAction` functions.
|
||||
|
||||
These functions are the primary mechanism for data handling when using [SPA mode][spa]. This guide demonstrates common use cases for leveraging client data in Server-Side Rendering (SSR).
|
||||
|
||||
## Skip the Server Hop
|
||||
|
||||
When using React Router with a Backend-For-Frontend (BFF) architecture, you might want to bypass the React Router server and communicate directly with your backend API. This approach requires proper authentication handling and assumes no CORS restrictions. Here's how to implement this:
|
||||
|
||||
1. Load the data from server `loader` on the document load
|
||||
2. Load the data from the `clientLoader` on all subsequent loads
|
||||
|
||||
In this scenario, React Router will _not_ call the `clientLoader` on hydration - and will only call it on subsequent navigations.
|
||||
|
||||
```tsx lines=[4,11]
|
||||
export async function loader({
|
||||
request,
|
||||
}: Route.LoaderArgs) {
|
||||
const data = await fetchApiFromServer({ request }); // (1)
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function clientLoader({
|
||||
request,
|
||||
}: Route.ClientLoaderArgs) {
|
||||
const data = await fetchApiFromClient({ request }); // (2)
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
## Fullstack State
|
||||
|
||||
Sometimes you need to combine data from both the server and browser (like IndexedDB or browser SDKs) before rendering a component. Here's how to implement this pattern:
|
||||
|
||||
1. Load the partial data from server `loader` on the document load
|
||||
2. Export a [`HydrateFallback`][hydratefallback] component to render during SSR because we don't yet have a full set of data
|
||||
3. Set `clientLoader.hydrate = true`, this instructs React Router to call the clientLoader as part of initial document hydration
|
||||
4. Combine the server data with the client data in `clientLoader`
|
||||
|
||||
```tsx lines=[4-6,19-20,23,26]
|
||||
export async function loader({
|
||||
request,
|
||||
}: Route.LoaderArgs) {
|
||||
const partialData = await getPartialDataFromDb({
|
||||
request,
|
||||
}); // (1)
|
||||
return partialData;
|
||||
}
|
||||
|
||||
export async function clientLoader({
|
||||
request,
|
||||
serverLoader,
|
||||
}: Route.ClientLoaderArgs) {
|
||||
const [serverData, clientData] = await Promise.all([
|
||||
serverLoader(),
|
||||
getClientData(request),
|
||||
]);
|
||||
return {
|
||||
...serverData, // (4)
|
||||
...clientData, // (4)
|
||||
};
|
||||
}
|
||||
clientLoader.hydrate = true as const; // (3)
|
||||
|
||||
export function HydrateFallback() {
|
||||
return <p>Skeleton rendered during SSR</p>; // (2)
|
||||
}
|
||||
|
||||
export default function Component({
|
||||
// This will always be the combined set of server + client data
|
||||
loaderData,
|
||||
}: Route.ComponentProps) {
|
||||
return <>...</>;
|
||||
}
|
||||
```
|
||||
|
||||
## Choosing Server or Client Data Loading
|
||||
|
||||
You can mix data loading strategies across your application, choosing between server-only or client-only data loading for each route. Here's how to implement both approaches:
|
||||
|
||||
1. Export a `loader` when you want to use server data
|
||||
2. Export `clientLoader` and a `HydrateFallback` when you want to use client data
|
||||
|
||||
A route that only depends on a server loader looks like this:
|
||||
|
||||
```tsx filename=app/routes/server-data-route.tsx
|
||||
export async function loader({
|
||||
request,
|
||||
}: Route.LoaderArgs) {
|
||||
const data = await getServerData(request);
|
||||
return data;
|
||||
}
|
||||
|
||||
export default function Component({
|
||||
loaderData, // (1) - server data
|
||||
}: Route.ComponentProps) {
|
||||
return <>...</>;
|
||||
}
|
||||
```
|
||||
|
||||
A route that only depends on a client loader looks like this.
|
||||
|
||||
```tsx filename=app/routes/client-data-route.tsx
|
||||
export async function clientLoader({
|
||||
request,
|
||||
}: Route.ClientLoaderArgs) {
|
||||
const clientData = await getClientData(request);
|
||||
return clientData;
|
||||
}
|
||||
// Note: you do not have to set this explicitly - it is implied if there is no `loader`
|
||||
clientLoader.hydrate = true;
|
||||
|
||||
// (2)
|
||||
export function HydrateFallback() {
|
||||
return <p>Skeleton rendered during SSR</p>;
|
||||
}
|
||||
|
||||
export default function Component({
|
||||
loaderData, // (2) - client data
|
||||
}: Route.ComponentProps) {
|
||||
return <>...</>;
|
||||
}
|
||||
```
|
||||
|
||||
## Client-Side Caching
|
||||
|
||||
You can implement client-side caching (using memory, localStorage, etc.) to optimize server requests. Here's a pattern that demonstrates cache management:
|
||||
|
||||
1. Load the data from server `loader` on the document load
|
||||
2. Set `clientLoader.hydrate = true` to prime the cache
|
||||
3. Load subsequent navigations from the cache via `clientLoader`
|
||||
4. Invalidate the cache in your `clientAction`
|
||||
|
||||
Note that since we are not exporting a `HydrateFallback` component, we will SSR the route component and then run the `clientLoader` on hydration, so it's important that your `loader` and `clientLoader` return the same data on initial load to avoid hydration errors.
|
||||
|
||||
```tsx lines=[4,26,32,39,46]
|
||||
export async function loader({
|
||||
request,
|
||||
}: Route.LoaderArgs) {
|
||||
const data = await getDataFromDb({ request }); // (1)
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function action({
|
||||
request,
|
||||
}: Route.ActionArgs) {
|
||||
await saveDataToDb({ request });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
let isInitialRequest = true;
|
||||
|
||||
export async function clientLoader({
|
||||
request,
|
||||
serverLoader,
|
||||
}: Route.ClientLoaderArgs) {
|
||||
const cacheKey = generateKey(request);
|
||||
|
||||
if (isInitialRequest) {
|
||||
isInitialRequest = false;
|
||||
const serverData = await serverLoader();
|
||||
cache.set(cacheKey, serverData); // (2)
|
||||
return serverData;
|
||||
}
|
||||
|
||||
const cachedData = await cache.get(cacheKey);
|
||||
if (cachedData) {
|
||||
return cachedData; // (3)
|
||||
}
|
||||
|
||||
const serverData = await serverLoader();
|
||||
cache.set(cacheKey, serverData);
|
||||
return serverData;
|
||||
}
|
||||
clientLoader.hydrate = true; // (2)
|
||||
|
||||
export async function clientAction({
|
||||
request,
|
||||
serverAction,
|
||||
}: Route.ClientActionArgs) {
|
||||
const cacheKey = generateKey(request);
|
||||
cache.delete(cacheKey); // (4)
|
||||
const serverData = await serverAction();
|
||||
return serverData;
|
||||
}
|
||||
```
|
||||
|
||||
[spa]: ../how-to/spa
|
||||
[hydratefallback]: ../start/framework/route-module#hydratefallback
|
||||
+317
@@ -0,0 +1,317 @@
|
||||
---
|
||||
title: Data Strategy
|
||||
---
|
||||
|
||||
# Data Strategy
|
||||
|
||||
[MODES: data]
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<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>
|
||||
|
||||
## Overview
|
||||
|
||||
By 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.
|
||||
|
||||
The [`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.
|
||||
|
||||
## Usage
|
||||
|
||||
A 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:
|
||||
|
||||
- `matches`: An array of `DataStrategyMatch` instances for the routes matched by the current `request`
|
||||
- `runClientMiddleware`: A helper function to run the middleware for the matched routes
|
||||
- `fetcherKey`: The fetcher key if this is for a fetcher request and not a navigation
|
||||
|
||||
A `DataStrategyMatch` is a normal route match plus a few additional fields:
|
||||
|
||||
- `shouldCallHandler`: A function that tells you whether this routes handler should be called for this request
|
||||
- `shouldRevalidateArgs`: The arguments that to be passed to the routes `shouldRevalidate` for this request
|
||||
- ~~`shouldLoad`~~: A boolean field for whether this routes handler should be run for this request
|
||||
- Deprecated in favor of the more powerful `shouldCallHandler` API
|
||||
- `resolve`: A function to handle call through to the route handler, and also allow you custom execution of the handler
|
||||
|
||||
Here's a basic example that adds logging around the handler executions:
|
||||
|
||||
```tsx
|
||||
let router = createBrowserRouter(routes, {
|
||||
async dataStrategy({
|
||||
matches,
|
||||
request,
|
||||
runClientMiddleware,
|
||||
}) {
|
||||
// Determine which matches are expected to be executed for this request.
|
||||
// - For loading navigations, this will return true for new routes + existing
|
||||
// routes requiring revalidation
|
||||
// - For submission navigations, this will only return true for the action route
|
||||
// - For fetcher calls, this will only return true for the fetcher route
|
||||
const matchesToLoad = matches.filter((m) =>
|
||||
m.shouldCallHandler(),
|
||||
);
|
||||
|
||||
// For each match that we want to execute, call match.resolve() to execute
|
||||
// the handler and store the result
|
||||
const results: Record<string, DataStrategyResult> = {};
|
||||
await runClientMiddleware(() =>
|
||||
Promise.all(
|
||||
matchesToLoad.map(async (match) => {
|
||||
console.log(`Processing ${match.route.id}`);
|
||||
// The resolve function calls through to the route handler
|
||||
results[match.route.id] = await match.resolve();
|
||||
}),
|
||||
),
|
||||
);
|
||||
return results;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The `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:
|
||||
|
||||
```ts
|
||||
interface DataStrategyResult {
|
||||
type: "data" | "error";
|
||||
result: unknown; // data, Error, Response, data()
|
||||
}
|
||||
```
|
||||
|
||||
### Calling Route Middleware
|
||||
|
||||
If you are using `middleware` on your routes, you need to leverage the `callClientMiddleware` helper function to execute `middleware` around your handlers:
|
||||
|
||||
```tsx
|
||||
let router = createBrowserRouter(routes, {
|
||||
async dataStrategy({
|
||||
matches,
|
||||
request,
|
||||
runClientMiddleware,
|
||||
}) {
|
||||
const matchesToLoad = matches.filter((m) =>
|
||||
m.shouldCallHandler(),
|
||||
);
|
||||
const results: Record<string, DataStrategyResult> = {};
|
||||
|
||||
// Run middleware and execute handlers at the end of the middleware chain
|
||||
await runClientMiddleware(() =>
|
||||
Promise.all(
|
||||
matchesToLoad.map(async (match) => {
|
||||
results[match.route.id] = await match.resolve();
|
||||
}),
|
||||
),
|
||||
);
|
||||
return results;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
`runClientMiddleware` takes the same arguments as `dataStrategy` so it can also be easily composed with a standalone `dataStrategy` implementation:
|
||||
|
||||
```tsx
|
||||
const loggingDataStrategy: DataStrategyFunction = () => {
|
||||
/* ... */
|
||||
};
|
||||
|
||||
let router = createBrowserRouter(routes, {
|
||||
async dataStrategy({ runClientMiddleware }) {
|
||||
let results = await runClientMiddleware(
|
||||
loggingDataStrategy,
|
||||
);
|
||||
return results;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Advanced handler execution
|
||||
|
||||
If you want more fine-grained control over the execution of the handler, you can pass a callback to `match.resolve()`:
|
||||
|
||||
```tsx
|
||||
// Assume a loader shape such as
|
||||
function loader({ request }, customContext) {...}
|
||||
|
||||
// In your dataStrategy, you can pass this context from inside a resolve callback
|
||||
await Promise.all(
|
||||
matchesToLoad.map((match, i) =>
|
||||
match.resolve((handler) => {
|
||||
let customContext = getCustomContext();
|
||||
// Call the handler and p[ass a custom parameter as the handler's second argument
|
||||
return handler(customContext);
|
||||
}),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### Custom Revalidation Behavior
|
||||
|
||||
If 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`:
|
||||
|
||||
```tsx
|
||||
const matchesToLoad = matches.filter((match) => {
|
||||
let defaultShouldRevalidate = customShouldRevalidate(
|
||||
match.shouldRevalidateArgs,
|
||||
);
|
||||
return m.shouldCallHandler(defaultShouldRevalidate);
|
||||
});
|
||||
```
|
||||
|
||||
## Migrating away from `shouldLoad`
|
||||
|
||||
Now 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.
|
||||
|
||||
The 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.
|
||||
|
||||
With `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.
|
||||
|
||||
Here's an example change from the prior API to the new API. Note that we pre-filter the `matchesToLoad` before calling `resolve()`:
|
||||
|
||||
```diff
|
||||
let results = {};
|
||||
+let matchesToLoad = matches.filter(m => m.shouldCallHandler());
|
||||
await Promise.all(() =>
|
||||
- matches.map((m) => {
|
||||
+ matchesToLoad.map((m) => {
|
||||
results[m.route.id] = await m.resolve();
|
||||
}),
|
||||
);
|
||||
return results;
|
||||
```
|
||||
|
||||
## Advanced Use Cases
|
||||
|
||||
### Custom Middleware
|
||||
|
||||
<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>
|
||||
|
||||
Let's define a middleware on each route via [`handle`](../start/data/route-object#handle)
|
||||
and call middleware sequentially first, then call all
|
||||
[`loader`](../start/data/route-object#loader)s in parallel - providing
|
||||
any data made available via the middleware:
|
||||
|
||||
```ts
|
||||
const routes = [
|
||||
{
|
||||
id: "parent",
|
||||
path: "/parent",
|
||||
loader({ request }, context) {
|
||||
// ...
|
||||
},
|
||||
handle: {
|
||||
async middleware({ request }, context) {
|
||||
context.parent = "PARENT MIDDLEWARE";
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: "child",
|
||||
path: "child",
|
||||
loader({ request }, context) {
|
||||
// ...
|
||||
},
|
||||
handle: {
|
||||
async middleware({ request }, context) {
|
||||
context.child = "CHILD MIDDLEWARE";
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
let router = createBrowserRouter(routes, {
|
||||
async dataStrategy({ matches, params, request }) {
|
||||
// Run middleware sequentially and let them add data to `context`
|
||||
let context = {};
|
||||
for (const match of matches) {
|
||||
if (match.route.handle?.middleware) {
|
||||
await match.route.handle.middleware(
|
||||
{ request, params },
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Run loaders in parallel with the `context` value
|
||||
let matchesToLoad = matches.filter((m) =>
|
||||
m.shouldCallHandler(),
|
||||
);
|
||||
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
|
||||
return handler(context);
|
||||
}),
|
||||
),
|
||||
);
|
||||
return results.reduce(
|
||||
(acc, result, i) =>
|
||||
Object.assign(acc, {
|
||||
[matchesToLoad[i].route.id]: result,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Handler
|
||||
|
||||
It's also possible you don't even want to define a [`loader`](../start/data/route-object#loader)
|
||||
implementation at the route level. Maybe you want to just determine the
|
||||
routes and issue a single GraphQL request for all of your data. You can do
|
||||
that by setting your `route.loader=true` so it qualifies as "having a
|
||||
loader", and then store GQL fragments on `route.handle`:
|
||||
|
||||
```ts
|
||||
const routes = [
|
||||
{
|
||||
id: "parent",
|
||||
path: "/parent",
|
||||
loader: true,
|
||||
handle: {
|
||||
gql: gql`
|
||||
fragment Parent on Whatever {
|
||||
parentField
|
||||
}
|
||||
`,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: "child",
|
||||
path: "child",
|
||||
loader: true,
|
||||
handle: {
|
||||
gql: gql`
|
||||
fragment Child on Whatever {
|
||||
childField
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
let router = createBrowserRouter(routes, {
|
||||
async dataStrategy({ matches, params, request }) {
|
||||
const matchesToLoad = matches.filter((m) =>
|
||||
m.shouldCallHandler(),
|
||||
);
|
||||
// Compose route fragments into a single GQL payload
|
||||
let gql = getFragmentsFromRouteHandles(matchesToLoad);
|
||||
let data = await fetchGql(gql);
|
||||
// Parse results back out into individual route level `DataStrategyResult`'s
|
||||
// keyed by `routeId`
|
||||
let results = parseResultsFromGql(matchesToLoad, data);
|
||||
return results;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Note 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`.
|
||||
|
||||
[loader]: ../start/data/route-object#loader
|
||||
[action]: ../start/data/route-object#action
|
||||
[data-strategy]: ../api/data-routers/createBrowserRouter#optsdatastrategy
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
---
|
||||
title: Error Boundaries
|
||||
---
|
||||
|
||||
# Error Boundaries
|
||||
|
||||
[MODES: framework, data]
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
To avoid rendering an empty page to users, route modules will automatically catch errors in your code and render the closest `ErrorBoundary`.
|
||||
|
||||
Error boundaries are not intended for rendering form validation errors or error reporting. Please see [Form Validation](./form-validation) and [Error Reporting](./error-reporting) instead.
|
||||
|
||||
## 1. Add a root error boundary
|
||||
|
||||
All applications should at a minimum export a root error boundary. This one handles the three main cases:
|
||||
|
||||
- Thrown `data` with a status code and text
|
||||
- Instances of errors with a stack trace
|
||||
- Randomly thrown values
|
||||
|
||||
### Framework Mode
|
||||
|
||||
[modes: framework]
|
||||
|
||||
In [Framework Mode][picking-a-mode], errors are passed to the route-level error boundary as a prop (see [`Route.ErrorBoundaryProps`][type-safety]), so you don't need to use a hook to grab it:
|
||||
|
||||
```tsx filename=root.tsx lines=[1,3-5]
|
||||
import { Route } from "./+types/root";
|
||||
|
||||
export function ErrorBoundary({
|
||||
error,
|
||||
}: Route.ErrorBoundaryProps) {
|
||||
if (isRouteErrorResponse(error)) {
|
||||
return (
|
||||
<>
|
||||
<h1>
|
||||
{error.status} {error.statusText}
|
||||
</h1>
|
||||
<p>{error.data}</p>
|
||||
</>
|
||||
);
|
||||
} else if (error instanceof Error) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Error</h1>
|
||||
<p>{error.message}</p>
|
||||
<p>The stack trace is:</p>
|
||||
<pre>{error.stack}</pre>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <h1>Unknown Error</h1>;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Data Mode
|
||||
|
||||
[modes: data]
|
||||
|
||||
In [Data Mode][picking-a-mode], the `ErrorBoundary` doesn't receive props, so you can access it via `useRouteError`:
|
||||
|
||||
```tsx lines=[1,6,16]
|
||||
import { useRouteError } from "react-router";
|
||||
|
||||
let router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
ErrorBoundary: RootErrorBoundary,
|
||||
Component: Root,
|
||||
},
|
||||
]);
|
||||
|
||||
function Root() {
|
||||
/* ... */
|
||||
}
|
||||
|
||||
function RootErrorBoundary() {
|
||||
let error = useRouteError();
|
||||
if (isRouteErrorResponse(error)) {
|
||||
return (
|
||||
<>
|
||||
<h1>
|
||||
{error.status} {error.statusText}
|
||||
</h1>
|
||||
<p>{error.data}</p>
|
||||
</>
|
||||
);
|
||||
} else if (error instanceof Error) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Error</h1>
|
||||
<p>{error.message}</p>
|
||||
<p>The stack trace is:</p>
|
||||
<pre>{error.stack}</pre>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <h1>Unknown Error</h1>;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2. Write a bug
|
||||
|
||||
[modes: framework,data]
|
||||
|
||||
It's not recommended to intentionally throw errors to force the error boundary to render as a means of control flow. Error Boundaries are primarily for catching unintentional errors in your code.
|
||||
|
||||
```tsx
|
||||
export async function loader() {
|
||||
return undefined();
|
||||
}
|
||||
```
|
||||
|
||||
This will render the `instanceof Error` branch of the UI from step 1.
|
||||
|
||||
This is not just for loaders, but for all route module APIs: loaders, actions, components, headers, links, and meta.
|
||||
|
||||
## 3. Throw data in loaders/actions
|
||||
|
||||
[modes: framework,data]
|
||||
|
||||
There are exceptions to the rule in #2, especially 404s. You can intentionally `throw data()` (with a proper status code) to the closest error boundary when your loader can't find what it needs to render the page. Throw a 404 and move on.
|
||||
|
||||
```tsx
|
||||
import { data } from "react-router";
|
||||
|
||||
export async function loader({ params }) {
|
||||
let record = await fakeDb.getRecord(params.id);
|
||||
if (!record) {
|
||||
throw data("Record Not Found", { status: 404 });
|
||||
}
|
||||
return record;
|
||||
}
|
||||
```
|
||||
|
||||
This will render the `isRouteErrorResponse` branch of the UI from step 1.
|
||||
|
||||
## 4. Nested error boundaries
|
||||
|
||||
When an error is thrown, the "closest error boundary" will be rendered.
|
||||
|
||||
### Framework Mode
|
||||
|
||||
[modes: framework]
|
||||
|
||||
Consider these nested routes:
|
||||
|
||||
```tsx filename="routes.ts"
|
||||
// ✅ has error boundary
|
||||
route("/app", "app.tsx", [
|
||||
// ❌ no error boundary
|
||||
route("invoices", "invoices.tsx", [
|
||||
// ✅ has error boundary
|
||||
route("invoices/:id", "invoice-page.tsx", [
|
||||
// ❌ no error boundary
|
||||
route("payments", "payments.tsx"),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
```
|
||||
|
||||
The following table shows which error boundary will render given the origin of the error:
|
||||
|
||||
| error origin | rendered boundary |
|
||||
| ---------------- | ----------------- |
|
||||
| app.tsx | app.tsx |
|
||||
| invoices.tsx | app.tsx |
|
||||
| invoice-page.tsx | invoice-page.tsx |
|
||||
| payments.tsx | invoice-page.tsx |
|
||||
|
||||
### Data Mode
|
||||
|
||||
[modes: data]
|
||||
|
||||
In Data Mode, the equivalent route tree might look like:
|
||||
|
||||
```tsx
|
||||
let router = createBrowserRouter([
|
||||
{
|
||||
path: "/app",
|
||||
Component: App,
|
||||
ErrorBoundary: AppErrorBoundary, // ✅ has error boundary
|
||||
children: [
|
||||
{
|
||||
path: "invoices",
|
||||
Component: Invoices, // ❌ no error boundary
|
||||
children: [
|
||||
{
|
||||
path: ":id",
|
||||
Component: Invoice,
|
||||
ErrorBoundary: InvoiceErrorBoundary, // ✅ has error boundary
|
||||
children: [
|
||||
{
|
||||
path: "payments",
|
||||
Component: Payments, // ❌ no error boundary
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
The following table shows which error boundary will render given the origin of the error:
|
||||
|
||||
| error origin | rendered boundary |
|
||||
| ------------ | ---------------------- |
|
||||
| `App` | `AppErrorBoundary` |
|
||||
| `Invoices` | `AppErrorBoundary` |
|
||||
| `Invoice` | `InvoiceErrorBoundary` |
|
||||
| `Payments` | `InvoiceErrorBoundary` |
|
||||
|
||||
## Error Sanitization
|
||||
|
||||
[modes: framework]
|
||||
|
||||
In Framework Mode when building for production, any errors that happen on the server are automatically sanitized before being sent to the browser to prevent leaking any sensitive server information (like stack traces).
|
||||
|
||||
This means that a thrown `Error` will have a generic message and no stack trace in production in the browser. The original error is untouched on the server.
|
||||
|
||||
Also note that data sent with `throw data(yourData)` is not sanitized as the data there is intended to be rendered.
|
||||
|
||||
[picking-a-mode]: ../start/modes
|
||||
[type-safety]: ../explanation/type-safety
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
---
|
||||
title: Error Reporting
|
||||
---
|
||||
|
||||
# Error Reporting
|
||||
|
||||
[MODES: framework,data]
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
React Router catches errors in your route modules and sends them to [error boundaries](./error-boundary) to prevent blank pages when errors occur. However, `ErrorBoundary` isn't sufficient for logging and reporting errors.
|
||||
|
||||
## Server Errors
|
||||
|
||||
[modes: framework]
|
||||
|
||||
To access these caught errors on the server, use the `handleError` export of the server entry module.
|
||||
|
||||
### 1. Reveal the server entry
|
||||
|
||||
If you don't see [`entry.server.tsx`][entryserver] in your app directory, you're using a default entry. Reveal it with this cli command:
|
||||
|
||||
```shellscript nonumber
|
||||
react-router reveal entry.server
|
||||
```
|
||||
|
||||
### 2. Export your error handler
|
||||
|
||||
This function is called whenever React Router catches an error in your application on the server.
|
||||
|
||||
```tsx filename=entry.server.tsx
|
||||
import { type HandleErrorFunction } from "react-router";
|
||||
|
||||
export const handleError: HandleErrorFunction = (
|
||||
error,
|
||||
{ request },
|
||||
) => {
|
||||
// React Router may abort some interrupted requests, don't log those
|
||||
if (!request.signal.aborted) {
|
||||
myReportError(error);
|
||||
|
||||
// make sure to still log the error so you can see it
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
See also:
|
||||
|
||||
- [`handleError`][handleError]
|
||||
|
||||
## Client Errors
|
||||
|
||||
To access these caught errors on the client, use the `onError` prop on your [`HydratedRouter`][hydratedrouter] or [`RouterProvider`][routerprovider] component.
|
||||
|
||||
### Framework Mode
|
||||
|
||||
[modes: framework]
|
||||
|
||||
#### 1. Reveal the client entry
|
||||
|
||||
If you don't see [`entry.client.tsx`][entryclient] in your app directory, you're using a default entry. Reveal it with this cli command:
|
||||
|
||||
```shellscript nonumber
|
||||
react-router reveal entry.client
|
||||
```
|
||||
|
||||
#### 2. Add your error handler
|
||||
|
||||
This function is called whenever React Router catches an error in your application on the client.
|
||||
|
||||
```tsx filename=entry.client.tsx
|
||||
import { type ClientOnErrorFunction } from "react-router";
|
||||
|
||||
const onError: ClientOnErrorFunction = (
|
||||
error,
|
||||
{ location, params, pattern, errorInfo },
|
||||
) => {
|
||||
myReportError(error, location, errorInfo);
|
||||
|
||||
// make sure to still log the error so you can see it
|
||||
console.error(error, errorInfo);
|
||||
};
|
||||
|
||||
startTransition(() => {
|
||||
hydrateRoot(
|
||||
document,
|
||||
<StrictMode>
|
||||
<HydratedRouter onError={onError} />
|
||||
</StrictMode>,
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
See also:
|
||||
|
||||
- [`<HydratedRouter onError>`][hydratedrouter-onerror]
|
||||
|
||||
### Data Mode
|
||||
|
||||
[modes: data]
|
||||
|
||||
This function is called whenever React Router catches an error in your application on the client.
|
||||
|
||||
```tsx
|
||||
import {
|
||||
createBrowserRouter,
|
||||
type ClientOnErrorFunction,
|
||||
} from "react-router";
|
||||
import { RouterProvider } from "react-router/dom";
|
||||
|
||||
const onError: ClientOnErrorFunction = (
|
||||
error,
|
||||
{ location, params, pattern, errorInfo },
|
||||
) => {
|
||||
myReportError(error, location, errorInfo);
|
||||
|
||||
// make sure to still log the error so you can see it
|
||||
console.error(error, errorInfo);
|
||||
};
|
||||
|
||||
const router = createBrowserRouter(routes);
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<RouterProvider router={router} onError={onError} />
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
See also:
|
||||
|
||||
- [`<RouterProvider onError>`][routerprovider-onerror]
|
||||
|
||||
[entryserver]: ../api/framework-conventions/entry.server.tsx
|
||||
[handleError]: ../api/framework-conventions/entry.server.tsx#handleerror
|
||||
[entryclient]: ../api/framework-conventions/entry.client.tsx
|
||||
[hydratedrouter]: ../api/framework-routers/HydratedRouter
|
||||
[routerprovider]: ../api/data-routers/RouterProvider
|
||||
[hydratedrouter-onerror]: ../api/framework-routers/HydratedRouter#onError
|
||||
[routerprovider-onerror]: ../api/data-routers/RouterProvider#onError
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
---
|
||||
title: Using Fetchers
|
||||
---
|
||||
|
||||
# Using Fetchers
|
||||
|
||||
[MODES: framework, data]
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
Fetchers are useful for creating complex, dynamic user interfaces that require multiple, concurrent data interactions without causing a navigation.
|
||||
|
||||
Fetchers track their own, independent state and can be used to load data, mutate data, submit forms, and generally interact with loaders and actions.
|
||||
|
||||
## Calling Actions
|
||||
|
||||
The most common case for a fetcher is to submit data to an action, triggering a revalidation of route data. Consider the following route module:
|
||||
|
||||
```tsx
|
||||
import { useLoaderData } from "react-router";
|
||||
|
||||
export async function clientLoader({ request }) {
|
||||
let title = localStorage.getItem("title") || "No Title";
|
||||
return { title };
|
||||
}
|
||||
|
||||
export default function Component() {
|
||||
let data = useLoaderData();
|
||||
return (
|
||||
<div>
|
||||
<h1>{data.title}</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 1. Add an action
|
||||
|
||||
First we'll add an action to the route for the fetcher to call:
|
||||
|
||||
```tsx lines=[7-11]
|
||||
import { useLoaderData } from "react-router";
|
||||
|
||||
export async function clientLoader({ request }) {
|
||||
// ...
|
||||
}
|
||||
|
||||
export async function clientAction({ request }) {
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
let data = await request.formData();
|
||||
localStorage.setItem("title", data.get("title"));
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export default function Component() {
|
||||
let data = useLoaderData();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Create a fetcher
|
||||
|
||||
Next create a fetcher and render a form with it:
|
||||
|
||||
```tsx lines=[7,12-14]
|
||||
import { useLoaderData, useFetcher } from "react-router";
|
||||
|
||||
// ...
|
||||
|
||||
export default function Component() {
|
||||
let data = useLoaderData();
|
||||
let fetcher = useFetcher();
|
||||
return (
|
||||
<div>
|
||||
<h1>{data.title}</h1>
|
||||
|
||||
<fetcher.Form method="post">
|
||||
<input type="text" name="title" />
|
||||
</fetcher.Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Submit the form
|
||||
|
||||
If you submit the form now, the fetcher will call the action and revalidate the route data automatically.
|
||||
|
||||
### 4. Render pending state
|
||||
|
||||
Fetchers make their state available during the async work so you can render pending UI the moment the user interacts:
|
||||
|
||||
```tsx lines=[10]
|
||||
export default function Component() {
|
||||
let data = useLoaderData();
|
||||
let fetcher = useFetcher();
|
||||
return (
|
||||
<div>
|
||||
<h1>{data.title}</h1>
|
||||
|
||||
<fetcher.Form method="post">
|
||||
<input type="text" name="title" />
|
||||
{fetcher.state !== "idle" && <p>Saving...</p>}
|
||||
</fetcher.Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Optimistic UI
|
||||
|
||||
Sometimes there's enough information in the form to render the next state immediately. You can access the form data with `fetcher.formData`:
|
||||
|
||||
```tsx lines=[3-4,8]
|
||||
export default function Component() {
|
||||
let data = useLoaderData();
|
||||
let fetcher = useFetcher();
|
||||
let title = fetcher.formData?.get("title") || data.title;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{title}</h1>
|
||||
|
||||
<fetcher.Form method="post">
|
||||
<input type="text" name="title" />
|
||||
{fetcher.state !== "idle" && <p>Saving...</p>}
|
||||
</fetcher.Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Fetcher Data and Validation
|
||||
|
||||
Data returned from an action is available in the fetcher's `data` property. This is primarily useful for returning error messages to the user for a failed mutation:
|
||||
|
||||
```tsx lines=[7-10,28-32]
|
||||
// ...
|
||||
|
||||
export async function clientAction({ request }) {
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
let data = await request.formData();
|
||||
|
||||
let title = data.get("title") as string;
|
||||
if (title.trim() === "") {
|
||||
return { ok: false, error: "Title cannot be empty" };
|
||||
}
|
||||
|
||||
localStorage.setItem("title", title);
|
||||
return { ok: true, error: null };
|
||||
}
|
||||
|
||||
export default function Component() {
|
||||
let data = useLoaderData();
|
||||
let fetcher = useFetcher();
|
||||
let title = fetcher.formData?.get("title") || data.title;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{title}</h1>
|
||||
|
||||
<fetcher.Form method="post">
|
||||
<input type="text" name="title" />
|
||||
{fetcher.state !== "idle" && <p>Saving...</p>}
|
||||
{fetcher.data?.error && (
|
||||
<p style={{ color: "red" }}>
|
||||
{fetcher.data.error}
|
||||
</p>
|
||||
)}
|
||||
</fetcher.Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Loading Data
|
||||
|
||||
Another common use case for fetchers is to load data from a route for something like a combobox.
|
||||
|
||||
### 1. Create a search route
|
||||
|
||||
Consider the following route with a very basic search:
|
||||
|
||||
```tsx filename=./search-users.tsx
|
||||
// { path: '/search-users', filename: './search-users.tsx' }
|
||||
const users = [
|
||||
{ id: 1, name: "Ryan" },
|
||||
{ id: 2, name: "Michael" },
|
||||
// ...
|
||||
];
|
||||
|
||||
export async function loader({ request }) {
|
||||
await new Promise((res) => setTimeout(res, 300));
|
||||
let url = new URL(request.url);
|
||||
let query = url.searchParams.get("q");
|
||||
return users.filter((user) =>
|
||||
user.name.toLowerCase().includes(query.toLowerCase()),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Render a fetcher in a combobox component
|
||||
|
||||
```tsx
|
||||
import { useFetcher } from "react-router";
|
||||
|
||||
export function UserSearchCombobox() {
|
||||
let fetcher = useFetcher();
|
||||
return (
|
||||
<div>
|
||||
<fetcher.Form method="get" action="/search-users">
|
||||
<input type="text" name="q" />
|
||||
</fetcher.Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- The action points to the route we created above: "/search-users"
|
||||
- The name of the input is "q" to match the query parameter
|
||||
|
||||
### 3. Add type inference
|
||||
|
||||
```tsx lines=[2,5]
|
||||
import { useFetcher } from "react-router";
|
||||
import type { loader } from "./search-users";
|
||||
|
||||
export function UserSearchCombobox() {
|
||||
let fetcher = useFetcher<typeof loader>();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Ensure you use `import type` so you only import the types.
|
||||
|
||||
### 4. Render the data
|
||||
|
||||
```tsx lines=[10-16]
|
||||
import { useFetcher } from "react-router";
|
||||
|
||||
export function UserSearchCombobox() {
|
||||
let fetcher = useFetcher<typeof loader>();
|
||||
return (
|
||||
<div>
|
||||
<fetcher.Form method="get" action="/search-users">
|
||||
<input type="text" name="q" />
|
||||
</fetcher.Form>
|
||||
{fetcher.data && (
|
||||
<ul>
|
||||
{fetcher.data.map((user) => (
|
||||
<li key={user.id}>{user.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Note you will need to hit "enter" to submit the form and see the results.
|
||||
|
||||
### 5. Render a pending state
|
||||
|
||||
```tsx lines=[12-14]
|
||||
import { useFetcher } from "react-router";
|
||||
|
||||
export function UserSearchCombobox() {
|
||||
let fetcher = useFetcher<typeof loader>();
|
||||
return (
|
||||
<div>
|
||||
<fetcher.Form method="get" action="/search-users">
|
||||
<input type="text" name="q" />
|
||||
</fetcher.Form>
|
||||
{fetcher.data && (
|
||||
<ul
|
||||
style={{
|
||||
opacity: fetcher.state === "idle" ? 1 : 0.25,
|
||||
}}
|
||||
>
|
||||
{fetcher.data.map((user) => (
|
||||
<li key={user.id}>{user.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Search on user input
|
||||
|
||||
Fetchers can be submitted programmatically with `fetcher.submit`:
|
||||
|
||||
```tsx lines=[5-7]
|
||||
<fetcher.Form method="get" action="/search-users">
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
onChange={(event) => {
|
||||
fetcher.submit(event.currentTarget.form);
|
||||
}}
|
||||
/>
|
||||
</fetcher.Form>
|
||||
```
|
||||
|
||||
Note the input event's form is passed as the first argument to `fetcher.submit`. The fetcher will use that form to submit the request, reading its attributes and serializing the data from its elements.
|
||||
+410
@@ -0,0 +1,410 @@
|
||||
---
|
||||
title: File Route Conventions
|
||||
---
|
||||
|
||||
# File Route Conventions
|
||||
|
||||
[MODES: framework]
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
The `@react-router/fs-routes` package enables file-convention based route config.
|
||||
|
||||
## Setting up
|
||||
|
||||
First install the `@react-router/fs-routes` package:
|
||||
|
||||
```shellscript nonumber
|
||||
npm i @react-router/fs-routes
|
||||
```
|
||||
|
||||
Then use it to provide route config in your `app/routes.ts` file:
|
||||
|
||||
```tsx filename=app/routes.ts
|
||||
import { type RouteConfig } from "@react-router/dev/routes";
|
||||
import { flatRoutes } from "@react-router/fs-routes";
|
||||
|
||||
export default flatRoutes() satisfies RouteConfig;
|
||||
```
|
||||
|
||||
Any modules in the `app/routes` directory will become routes in your application by default.
|
||||
The `ignoredRouteFiles` option allows you to specify files that should not be included as routes:
|
||||
|
||||
```tsx filename=app/routes.ts
|
||||
import { type RouteConfig } from "@react-router/dev/routes";
|
||||
import { flatRoutes } from "@react-router/fs-routes";
|
||||
|
||||
export default flatRoutes({
|
||||
ignoredRouteFiles: ["home.tsx"],
|
||||
}) satisfies RouteConfig;
|
||||
```
|
||||
|
||||
This will look for routes in the `app/routes` directory by default, but this can be configured via the `rootDirectory` option which is relative to your app directory:
|
||||
|
||||
```tsx filename=app/routes.ts
|
||||
import { type RouteConfig } from "@react-router/dev/routes";
|
||||
import { flatRoutes } from "@react-router/fs-routes";
|
||||
|
||||
export default flatRoutes({
|
||||
rootDirectory: "file-routes",
|
||||
}) satisfies RouteConfig;
|
||||
```
|
||||
|
||||
The rest of this guide will assume you're using the default `app/routes` directory.
|
||||
|
||||
## Basic Routes
|
||||
|
||||
The filename maps to the route's URL pathname, except for `_index.tsx` which is the [index route][index_route] for the [root route][root_route]. You can use `.js`, `.jsx`, `.ts` or `.tsx` file extensions.
|
||||
|
||||
```text lines=[3-4]
|
||||
app/
|
||||
├── routes/
|
||||
│ ├── _index.tsx
|
||||
│ └── about.tsx
|
||||
└── root.tsx
|
||||
```
|
||||
|
||||
| URL | Matched Routes |
|
||||
| -------- | ----------------------- |
|
||||
| `/` | `app/routes/_index.tsx` |
|
||||
| `/about` | `app/routes/about.tsx` |
|
||||
|
||||
Note that these routes will be rendered in the outlet of `app/root.tsx` because of [nested routing][nested_routing].
|
||||
|
||||
## Dot Delimiters
|
||||
|
||||
Adding a `.` to a route filename will create a `/` in the URL.
|
||||
|
||||
```text lines=[5-7]
|
||||
app/
|
||||
├── routes/
|
||||
│ ├── _index.tsx
|
||||
│ ├── about.tsx
|
||||
│ ├── concerts.trending.tsx
|
||||
│ ├── concerts.salt-lake-city.tsx
|
||||
│ └── concerts.san-diego.tsx
|
||||
└── root.tsx
|
||||
```
|
||||
|
||||
| URL | Matched Route |
|
||||
| -------------------------- | ---------------------------------------- |
|
||||
| `/` | `app/routes/_index.tsx` |
|
||||
| `/about` | `app/routes/about.tsx` |
|
||||
| `/concerts/trending` | `app/routes/concerts.trending.tsx` |
|
||||
| `/concerts/salt-lake-city` | `app/routes/concerts.salt-lake-city.tsx` |
|
||||
| `/concerts/san-diego` | `app/routes/concerts.san-diego.tsx` |
|
||||
|
||||
The dot delimiter also creates nesting, see the [nesting section][nested_routes] for more information.
|
||||
|
||||
## Dynamic Segments
|
||||
|
||||
Usually your URLs aren't static but data-driven. Dynamic segments allow you to match segments of the URL and use that value in your code. You create them with the `$` prefix.
|
||||
|
||||
```text lines=[5]
|
||||
app/
|
||||
├── routes/
|
||||
│ ├── _index.tsx
|
||||
│ ├── about.tsx
|
||||
│ ├── concerts.$city.tsx
|
||||
│ └── concerts.trending.tsx
|
||||
└── root.tsx
|
||||
```
|
||||
|
||||
| URL | Matched Route |
|
||||
| -------------------------- | ---------------------------------- |
|
||||
| `/` | `app/routes/_index.tsx` |
|
||||
| `/about` | `app/routes/about.tsx` |
|
||||
| `/concerts/trending` | `app/routes/concerts.trending.tsx` |
|
||||
| `/concerts/salt-lake-city` | `app/routes/concerts.$city.tsx` |
|
||||
| `/concerts/san-diego` | `app/routes/concerts.$city.tsx` |
|
||||
|
||||
The value will be parsed from the URL and passed to various APIs. We call these values "URL Parameters". The most useful places to access the URL params are in [loaders] and [actions].
|
||||
|
||||
```tsx
|
||||
export async function loader({ params }) {
|
||||
return fakeDb.getAllConcertsForCity(params.city);
|
||||
}
|
||||
```
|
||||
|
||||
You'll note the property name on the `params` object maps directly to the name of your file: `$city.tsx` becomes `params.city`.
|
||||
|
||||
Routes can have multiple dynamic segments, like `concerts.$city.$date`, both are accessed on the params object by name:
|
||||
|
||||
```tsx
|
||||
export async function loader({ params }) {
|
||||
return fake.db.getConcerts({
|
||||
date: params.date,
|
||||
city: params.city,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
See the [routing guide][routing_guide] for more information.
|
||||
|
||||
## Nested Routes
|
||||
|
||||
Nested Routing is the general idea of coupling segments of the URL to component hierarchy and data. You can read more about it in the [Routing Guide][nested_routing].
|
||||
|
||||
You create nested routes with [dot delimiters][dot_delimiters]. If the filename before the `.` matches another route filename, it automatically becomes a child route to the matching parent. Consider these routes:
|
||||
|
||||
```text lines=[5-8]
|
||||
app/
|
||||
├── routes/
|
||||
│ ├── _index.tsx
|
||||
│ ├── about.tsx
|
||||
│ ├── concerts._index.tsx
|
||||
│ ├── concerts.$city.tsx
|
||||
│ ├── concerts.trending.tsx
|
||||
│ └── concerts.tsx
|
||||
└── root.tsx
|
||||
```
|
||||
|
||||
All the routes that start with `app/routes/concerts.` will be child routes of `app/routes/concerts.tsx` and render inside the [parent route's outlet][nested_routing].
|
||||
|
||||
| URL | Matched Route | Layout |
|
||||
| -------------------------- | ---------------------------------- | ------------------------- |
|
||||
| `/` | `app/routes/_index.tsx` | `app/root.tsx` |
|
||||
| `/about` | `app/routes/about.tsx` | `app/root.tsx` |
|
||||
| `/concerts` | `app/routes/concerts._index.tsx` | `app/routes/concerts.tsx` |
|
||||
| `/concerts/trending` | `app/routes/concerts.trending.tsx` | `app/routes/concerts.tsx` |
|
||||
| `/concerts/salt-lake-city` | `app/routes/concerts.$city.tsx` | `app/routes/concerts.tsx` |
|
||||
|
||||
Note you typically want to add an index route when you add nested routes so that something renders inside the parent's outlet when users visit the parent URL directly.
|
||||
|
||||
For example, if the URL is `/concerts/salt-lake-city` then the UI hierarchy will look like this:
|
||||
|
||||
```tsx
|
||||
<Root>
|
||||
<Concerts>
|
||||
<City />
|
||||
</Concerts>
|
||||
</Root>
|
||||
```
|
||||
|
||||
## Nested URLs without Layout Nesting
|
||||
|
||||
Sometimes you want the URL to be nested, but you don't want the automatic layout nesting. You can opt out of nesting with a trailing underscore on the parent segment:
|
||||
|
||||
```text lines=[8]
|
||||
app/
|
||||
├── routes/
|
||||
│ ├── _index.tsx
|
||||
│ ├── about.tsx
|
||||
│ ├── concerts.$city.tsx
|
||||
│ ├── concerts.trending.tsx
|
||||
│ ├── concerts.tsx
|
||||
│ └── concerts_.mine.tsx
|
||||
└── root.tsx
|
||||
```
|
||||
|
||||
| URL | Matched Route | Layout |
|
||||
| -------------------------- | ---------------------------------- | ------------------------- |
|
||||
| `/` | `app/routes/_index.tsx` | `app/root.tsx` |
|
||||
| `/about` | `app/routes/about.tsx` | `app/root.tsx` |
|
||||
| `/concerts/mine` | `app/routes/concerts_.mine.tsx` | `app/root.tsx` |
|
||||
| `/concerts/trending` | `app/routes/concerts.trending.tsx` | `app/routes/concerts.tsx` |
|
||||
| `/concerts/salt-lake-city` | `app/routes/concerts.$city.tsx` | `app/routes/concerts.tsx` |
|
||||
|
||||
Note that `/concerts/mine` does not nest with `app/routes/concerts.tsx` anymore, but `app/root.tsx`. The `trailing_` underscore creates a path segment, but it does not create layout nesting.
|
||||
|
||||
Think of the `trailing_` underscore as the long bit at the end of your parent's signature, writing you out of the will, removing the segment that follows from the layout nesting.
|
||||
|
||||
## Nested Layouts without Nested URLs
|
||||
|
||||
We call these <a name="pathless-routes"><b>Pathless Routes</b></a>
|
||||
|
||||
Sometimes you want to share a layout with a group of routes without adding any path segments to the URL. A common example is a set of authentication routes that have a different header/footer than the public pages or the logged in app experience. You can do this with a `_leading` underscore.
|
||||
|
||||
```text lines=[3-5]
|
||||
app/
|
||||
├── routes/
|
||||
│ ├── _auth.login.tsx
|
||||
│ ├── _auth.register.tsx
|
||||
│ ├── _auth.tsx
|
||||
│ ├── _index.tsx
|
||||
│ ├── concerts.$city.tsx
|
||||
│ └── concerts.tsx
|
||||
└── root.tsx
|
||||
```
|
||||
|
||||
| URL | Matched Route | Layout |
|
||||
| -------------------------- | ------------------------------- | ------------------------- |
|
||||
| `/` | `app/routes/_index.tsx` | `app/root.tsx` |
|
||||
| `/login` | `app/routes/_auth.login.tsx` | `app/routes/_auth.tsx` |
|
||||
| `/register` | `app/routes/_auth.register.tsx` | `app/routes/_auth.tsx` |
|
||||
| `/concerts` | `app/routes/concerts.tsx` | `app/routes/concerts.tsx` |
|
||||
| `/concerts/salt-lake-city` | `app/routes/concerts.$city.tsx` | `app/routes/concerts.tsx` |
|
||||
|
||||
Think of the `_leading` underscore as a blanket you're pulling over the filename, hiding the filename from the URL.
|
||||
|
||||
## Optional Segments
|
||||
|
||||
Wrapping a route segment in parentheses will make the segment optional.
|
||||
|
||||
```text lines=[3-5]
|
||||
app/
|
||||
├── routes/
|
||||
│ ├── ($lang)._index.tsx
|
||||
│ ├── ($lang).$productId.tsx
|
||||
│ └── ($lang).categories.tsx
|
||||
└── root.tsx
|
||||
```
|
||||
|
||||
| URL | Matched Route |
|
||||
| -------------------------- | ----------------------------------- |
|
||||
| `/` | `app/routes/($lang)._index.tsx` |
|
||||
| `/categories` | `app/routes/($lang).categories.tsx` |
|
||||
| `/en/categories` | `app/routes/($lang).categories.tsx` |
|
||||
| `/fr/categories` | `app/routes/($lang).categories.tsx` |
|
||||
| `/american-flag-speedo` | `app/routes/($lang)._index.tsx` |
|
||||
| `/en/american-flag-speedo` | `app/routes/($lang).$productId.tsx` |
|
||||
| `/fr/american-flag-speedo` | `app/routes/($lang).$productId.tsx` |
|
||||
|
||||
You may wonder why `/american-flag-speedo` is matching the `($lang)._index.tsx` route instead of `($lang).$productId.tsx`. This is because when you have an optional dynamic param segment followed by another dynamic param, it cannot reliably be determined if a single-segment URL such as `/american-flag-speedo` should match `/:lang` `/:productId`. Optional segments match eagerly and thus it will match `/:lang`. If you have this type of setup it's recommended to look at `params.lang` in the `($lang)._index.tsx` loader and redirect to `/:lang/american-flag-speedo` for the current/default language if `params.lang` is not a valid language code.
|
||||
|
||||
## Splat Routes
|
||||
|
||||
While [dynamic segments][dynamic_segments] match a single path segment (the stuff between two `/` in a URL), a splat route will match the rest of a URL, including the slashes.
|
||||
|
||||
```text lines=[4,6]
|
||||
app/
|
||||
├── routes/
|
||||
│ ├── _index.tsx
|
||||
│ ├── $.tsx
|
||||
│ ├── about.tsx
|
||||
│ └── files.$.tsx
|
||||
└── root.tsx
|
||||
```
|
||||
|
||||
| URL | Matched Route |
|
||||
| -------------------------------------------- | ------------------------ |
|
||||
| `/` | `app/routes/_index.tsx` |
|
||||
| `/about` | `app/routes/about.tsx` |
|
||||
| `/beef/and/cheese` | `app/routes/$.tsx` |
|
||||
| `/files` | `app/routes/files.$.tsx` |
|
||||
| `/files/talks/react-conf_old.pdf` | `app/routes/files.$.tsx` |
|
||||
| `/files/talks/react-conf_final.pdf` | `app/routes/files.$.tsx` |
|
||||
| `/files/talks/react-conf-FINAL-MAY_2024.pdf` | `app/routes/files.$.tsx` |
|
||||
|
||||
Similar to dynamic route parameters, you can access the value of the matched path on the splat route's `params` with the `"*"` key.
|
||||
|
||||
```tsx filename=app/routes/files.$.tsx
|
||||
export async function loader({ params }) {
|
||||
const filePath = params["*"];
|
||||
return fake.getFileInfo(filePath);
|
||||
}
|
||||
```
|
||||
|
||||
## Catch-all Route
|
||||
|
||||
To create a route that will match any requests that don't match other defined routes (such as a 404 page), create a file named `$.tsx` within your routes directory:
|
||||
|
||||
| URL | Matched Route |
|
||||
| ------------------------------ | ----------------------- |
|
||||
| `/` | `app/routes/_index.tsx` |
|
||||
| `/about` | `app/routes/about.tsx` |
|
||||
| `/any-invalid-path-will-match` | `app/routes/$.tsx` |
|
||||
|
||||
By default the matched route will return a 200 response, so be sure to modify your catchall route to return a 404 instead:
|
||||
|
||||
```tsx filename=app/routes/$.tsx
|
||||
export async function loader() {
|
||||
return data({}, 404);
|
||||
}
|
||||
```
|
||||
|
||||
## Escaping Special Characters
|
||||
|
||||
If you want one of the special characters used for these route conventions to actually be a part of the URL, you can escape the conventions with `[]` characters. This can be especially helpful for [resource routes][resource_routes] that include an extension in the URL.
|
||||
|
||||
| Filename | URL |
|
||||
| ----------------------------------- | ------------------- |
|
||||
| `app/routes/sitemap[.]xml.tsx` | `/sitemap.xml` |
|
||||
| `app/routes/[sitemap.xml].tsx` | `/sitemap.xml` |
|
||||
| `app/routes/weird-url.[_index].tsx` | `/weird-url/_index` |
|
||||
| `app/routes/dolla-bills-[$].tsx` | `/dolla-bills-$` |
|
||||
| `app/routes/[[so-weird]].tsx` | `/[so-weird]` |
|
||||
| `app/routes/reports.$id[.pdf].ts` | `/reports/123.pdf` |
|
||||
|
||||
## Folders for Organization
|
||||
|
||||
Routes can also be folders with a `route.tsx` file inside defining the route module. The rest of the files in the folder will not become routes. This allows you to organize your code closer to the routes that use them instead of repeating the feature names across other folders.
|
||||
|
||||
The files inside a folder have no meaning for the route paths, the route path is completely defined by the folder name.
|
||||
|
||||
Consider these routes:
|
||||
|
||||
```text
|
||||
app/
|
||||
├── routes/
|
||||
│ ├── _landing._index.tsx
|
||||
│ ├── _landing.about.tsx
|
||||
│ ├── _landing.tsx
|
||||
│ ├── app._index.tsx
|
||||
│ ├── app.projects.tsx
|
||||
│ ├── app.tsx
|
||||
│ └── app_.projects.$id.roadmap.tsx
|
||||
└── root.tsx
|
||||
```
|
||||
|
||||
Some, or all of them can be folders holding their own `route` module inside.
|
||||
|
||||
```text
|
||||
app/
|
||||
├── routes/
|
||||
│ ├── _landing._index/
|
||||
│ │ ├── route.tsx
|
||||
│ │ └── scroll-experience.tsx
|
||||
│ ├── _landing.about/
|
||||
│ │ ├── employee-profile-card.tsx
|
||||
│ │ ├── get-employee-data.server.ts
|
||||
│ │ ├── route.tsx
|
||||
│ │ └── team-photo.jpg
|
||||
│ ├── _landing/
|
||||
│ │ ├── footer.tsx
|
||||
│ │ ├── header.tsx
|
||||
│ │ └── route.tsx
|
||||
│ ├── app._index/
|
||||
│ │ ├── route.tsx
|
||||
│ │ └── stats.tsx
|
||||
│ ├── app.projects/
|
||||
│ │ ├── get-projects.server.ts
|
||||
│ │ ├── project-buttons.tsx
|
||||
│ │ ├── project-card.tsx
|
||||
│ │ └── route.tsx
|
||||
│ ├── app/
|
||||
│ │ ├── footer.tsx
|
||||
│ │ ├── primary-nav.tsx
|
||||
│ │ └── route.tsx
|
||||
│ ├── app_.projects.$id.roadmap/
|
||||
│ │ ├── chart.tsx
|
||||
│ │ ├── route.tsx
|
||||
│ │ └── update-timeline.server.ts
|
||||
│ └── contact-us.tsx
|
||||
└── root.tsx
|
||||
```
|
||||
|
||||
Note that when you turn a route module into a folder, the route module becomes `folder/route.tsx`, all other modules in the folder will not become routes. For example:
|
||||
|
||||
```
|
||||
# these are the same route:
|
||||
app/routes/app.tsx
|
||||
app/routes/app/route.tsx
|
||||
|
||||
# as are these
|
||||
app/routes/app._index.tsx
|
||||
app/routes/app._index/route.tsx
|
||||
```
|
||||
|
||||
[route-config-file]: ../start/framework/routing#configuring-routes
|
||||
[loaders]: ../start/framework/data-loading
|
||||
[actions]: ../start/framework/actions
|
||||
[routing_guide]: ../start/framework/routing
|
||||
[root_route]: ../start/framework/route-module
|
||||
[index_route]: ../start/framework/routing#index-routes
|
||||
[nested_routing]: ../start/framework/routing#nested-routes
|
||||
[nested_routes]: #nested-routes
|
||||
[dot_delimiters]: #dot-delimiters
|
||||
[dynamic_segments]: #dynamic-segments
|
||||
[resource_routes]: ../how-to/resource-routes
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user