From e8a5ac75a486d73af72e38262ab671a6d5fee1fa Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 11 Jul 2025 16:19:08 -0400 Subject: [PATCH 01/61] Generate markdown API docs from JSDoc (#13963) --- .eslintrc | 52 ++ docs/api/utils/Location.md | 18 - package.json | 3 + packages/react-router/lib/hooks.tsx | 853 +++++++++++++++++++--------- pnpm-lock.yaml | 187 +++++- scripts/docs.ts | 551 ++++++++++++++++++ 6 files changed, 1374 insertions(+), 290 deletions(-) delete mode 100644 docs/api/utils/Location.md create mode 100644 scripts/docs.ts diff --git a/.eslintrc b/.eslintrc index 61400497f8..62ab116a15 100644 --- a/.eslintrc +++ b/.eslintrc @@ -18,6 +18,58 @@ "env": { "jest/globals": false } + }, + { + // Only apply JSDoc lint rules to files we auto-generate docs for + "files": ["packages/react-router/lib/hooks.tsx"], + "plugins": ["jsdoc"], + "rules": { + "jsdoc/check-access": "error", + "jsdoc/check-alignment": "error", + "jsdoc/check-param-names": "error", + "jsdoc/check-property-names": "error", + "jsdoc/check-tag-names": [ + "error", + { + "definedTags": ["additionalExamples", "category", "mode"] + } + ], + "jsdoc/no-defaults": "error", + "jsdoc/no-multi-asterisks": ["error", { "allowWhitespace": true }], + "jsdoc/require-description": "error", + "jsdoc/require-param": ["error", { "enableRootFixer": false }], + "jsdoc/require-param-description": "error", + "jsdoc/require-param-name": "error", + "jsdoc/require-returns": "error", + "jsdoc/require-returns-check": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/sort-tags": [ + "error", + { + "tagSequence": [ + { + "tags": ["description"] + }, + { + "tags": ["example"] + }, + { + "tags": ["additionalExamples"] + }, + { + "tags": [ + "public", + "private", + "category", + "mode", + "param", + "returns" + ] + } + ] + } + ] + } } ], "reportUnusedDisableDirectives": true diff --git a/docs/api/utils/Location.md b/docs/api/utils/Location.md deleted file mode 100644 index 459f91710f..0000000000 --- a/docs/api/utils/Location.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: Location -hidden: true ---- - -# Location - -[MODES: framework, data, declarative] - -## Summary - -[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react_router.Location.html) - -An entry in a history stack. A location contains information about the -URL path, as well as possibly some arbitrary state and a key. - - - diff --git a/package.json b/package.json index 92a294ba71..ee70668137 100644 --- a/package.json +++ b/package.json @@ -65,14 +65,17 @@ "babel-jest": "^29.7.0", "babel-plugin-dev-expression": "^0.2.3", "chalk": "^4.1.2", + "dox": "^1.0.0", "eslint": "^8.57.0", "eslint-config-react-app": "^7.0.1", "eslint-plugin-flowtype": "^8.0.3", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^27.9.0", + "eslint-plugin-jsdoc": "^51.3.4", "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-react": "^7.34.1", "eslint-plugin-react-hooks": "next", + "fast-glob": "3.2.11", "isbot": "^5.1.11", "jest": "^29.6.4", "jsonfile": "^6.1.0", diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index 7d2eef2740..cb023e7f38 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -29,6 +29,7 @@ import type { RelativeRoutingType, Router as DataRouter, RevalidationState, + Navigation, } from "./router/router"; import { IDLE_BLOCKER } from "./router/router"; import type { @@ -52,18 +53,23 @@ import { import type { SerializeFrom } from "./types/route-data"; /** - Resolves a URL against the current location. - - ```tsx - import { useHref } from "react-router" - - function SomeComponent() { - let href = useHref("some/where"); - // "/resolved/some/where" - } - ``` - - @category Hooks + * Resolves a URL against the current location. + * + * @example + * import { useHref } from "react-router"; + * + * function SomeComponent() { + * let href = useHref("some/where"); + * // "/resolved/some/where" + * } + * + * @public + * @category Hooks + * @param to The path to resolve + * @param options Options + * @param options.relative Defaults to "route" so routing is relative to the route tree. + * Set to "path" to make relative routing operate against path segments. + * @returns The resolved href string */ export function useHref( to: To, @@ -97,34 +103,39 @@ export function useHref( * Returns true if this component is a descendant of a Router, useful to ensure * a component is used within a Router. * + * @public * @category Hooks + * @mode framework + * @mode data + * @returns Whether the component is within a Router context */ export function useInRouterContext(): boolean { return React.useContext(LocationContext) != null; } /** - Returns the current {@link Location}. This can be useful if you'd like to perform some side effect whenever it changes. - - ```tsx - import * as React from 'react' - import { useLocation } from 'react-router' - - function SomeComponent() { - let location = useLocation() - - React.useEffect(() => { - // Google Analytics - ga('send', 'pageview') - }, [location]); - - return ( - // ... - ); - } - ``` - - @category Hooks + * Returns the current {@link Location}. This can be useful if you'd like to perform some side effect whenever it changes. + * + * @example + * import * as React from 'react' + * import { useLocation } from 'react-router' + * + * function SomeComponent() { + * let location = useLocation() + * + * React.useEffect(() => { + * // Google Analytics + * ga('send', 'pageview') + * }, [location]); + * + * return ( + * // ... + * ); + * } + * + * @public + * @category Hooks + * @returns The current location object */ export function useLocation(): Location { invariant( @@ -141,7 +152,9 @@ export function useLocation(): Location { * Returns the current navigation action which describes how the router came to * the current location, either by a pop, push, or replace on the history stack. * + * @public * @category Hooks + * @returns The current navigation type (Action.Pop, Action.Push, or Action.Replace) */ export function useNavigationType(): NavigationType { return React.useContext(LocationContext).navigationType; @@ -152,7 +165,10 @@ export function useNavigationType(): NavigationType { * This is useful for components that need to know "active" state, e.g. * ``. * + * @public * @category Hooks + * @param pattern The pattern to match against the current location + * @returns The path match object if the pattern matches, null otherwise */ export function useMatch< ParamKey extends ParamParseKey, @@ -198,26 +214,99 @@ function useIsomorphicLayoutEffect( } /** - Returns a function that lets you navigate programmatically in the browser in response to user interactions or effects. - - ```tsx - import { useNavigate } from "react-router"; - - function SomeComponent() { - let navigate = useNavigate(); - return ( - + * ); + * } + * + * @additionalExamples + * ### Navigate to another path + * + * ```tsx + * navigate("/some/route"); + * navigate("/some/route?search=param"); + * ``` + * + * ### Navigate with a `To` object + * + * All properties are optional. + * + * ```tsx + * navigate({ + * pathname: "/some/route", + * search: "?search=param", + * hash: "#hash", + * state: { some: "state" }, + * }); + * ``` + * + * If you use `state`, that will be available on the `location` object on the next page. Access it with `useLocation().state` (see [useLocation](./useLocation)). + * + * ### Navigate back or forward in the history stack + * + * ```tsx + * // back + * // often used to close modals + * navigate(-1); + * + * // forward + * // often used in a multi-step wizard workflows + * navigate(1); + * ``` + * + * Be cautions with `navigate(number)`. If your application can load up to a route that has a button that tries to navigate forward/back, there may not be a history entry to go back or forward to, or it can go somewhere you don't expect (like a different domain). + * + * Only use this if you're sure they will have an entry in the history stack to navigate to. + * + * ### Replace the current entry in the history stack + * + * This will remove the current entry in the history stack, replacing it with a new one, similar to a server side redirect. + * + * ```tsx + * navigate("/some/route", { replace: true }); + * ``` + * + * ### Prevent Scroll Reset + * + * [MODES: framework, data] + * + *
+ *
+ * + * To prevent `` from resetting the scroll position, use the `preventScrollReset` option. + * + * ```tsx + * navigate("?some-tab=1", { preventScrollReset: true }); + * ``` + * + * For example, if you have a tab interface connected to search params in the middle of a page and you don't want it to scroll to the top when a tab is clicked. + * + * @public + * @category Hooks + * @returns A navigate function for programmatic navigation */ export function useNavigate(): NavigateFunction { let { isDataRoute } = React.useContext(RouteContext); @@ -300,9 +389,11 @@ function useNavigateUnstable(): NavigateFunction { const OutletContext = React.createContext(null); /** - * Returns the parent route {@link OutletProps.context | ``}. + * Returns the parent route {@link Outlet | ``}. * + * @public * @category Hooks + * @returns The context value passed to the Outlet */ export function useOutletContext(): Context { return React.useContext(OutletContext) as Context; @@ -312,7 +403,10 @@ export function useOutletContext(): Context { * Returns the element for the child route at this level of the route * hierarchy. Used internally by `` to render child routes. * + * @public * @category Hooks + * @param context The context to pass to the outlet + * @returns The child route element or null if no child routes match */ export function useOutlet(context?: unknown): React.ReactElement | null { let outlet = React.useContext(RouteContext).outlet; @@ -325,20 +419,105 @@ export function useOutlet(context?: unknown): React.ReactElement | null { } /** - Returns an object of key/value pairs of the dynamic params from the current URL that were matched by the routes. Child routes inherit all params from their parent routes. - - ```tsx - import { useParams } from "react-router" - - function SomeComponent() { - let params = useParams() - params.postId - } - ``` - - Assuming a route pattern like `/posts/:postId` is matched by `/posts/123` then `params.postId` will be `"123"`. - - @category Hooks + * Returns an object of key/value pairs of the dynamic params from the current URL that were matched by the routes. Child routes inherit all params from their parent routes. + * + * Assuming a route pattern like `/posts/:postId` is matched by `/posts/123` then `params.postId` will be `"123"`. + * + * @example + * import { useParams } from "react-router"; + * + * function SomeComponent() { + * let params = useParams(); + * params.postId; + * } + * + * @additionalExamples + * ### Basic Usage + * + * ```tsx + * import { useParams } from "react-router"; + * + * // given a route like: + * } />; + * + * // or a data route like: + * createBrowserRouter([ + * { + * path: "/posts/:postId", + * component: Post, + * }, + * ]); + * + * // or in routes.ts + * route("/posts/:postId", "routes/post.tsx"); + * ``` + * + * Access the params in a component: + * + * ```tsx + * import { useParams } from "react-router"; + * + * export default function Post() { + * let params = useParams(); + * return

Post: {params.postId}

; + * } + * ``` + * + * ### Multiple Params + * + * Patterns can have multiple params: + * + * ```tsx + * "/posts/:postId/comments/:commentId"; + * ``` + * + * All will be available in the params object: + * + * ```tsx + * import { useParams } from "react-router"; + * + * export default function Post() { + * let params = useParams(); + * return ( + *

+ * Post: {params.postId}, Comment: {params.commentId} + *

+ * ); + * } + * ``` + * + * ### Catchall Params + * + * Catchall params are defined with `*`: + * + * ```tsx + * "/files/*"; + * ``` + * + * The matched value will be available in the params object as follows: + * + * ```tsx + * import { useParams } from "react-router"; + * + * export default function File() { + * let params = useParams(); + * let catchall = params["*"]; + * // ... + * } + * ``` + * + * You can destructure the catchall param: + * + * ```tsx + * export default function File() { + * let { "*": catchall } = useParams(); + * console.log(catchall); + * } + * ``` + * + * @public + * @category Hooks + * @returns An object containing the dynamic route parameters */ export function useParams< ParamsOrKey extends string | Record = string @@ -351,21 +530,26 @@ export function useParams< } /** - Resolves the pathname of the given `to` value against the current location. Similar to {@link useHref}, but returns a {@link Path} instead of a string. - - ```tsx - import { useResolvedPath } from "react-router" - - function SomeComponent() { - // if the user is at /dashboard/profile - let path = useResolvedPath("../accounts") - path.pathname // "/dashboard/accounts" - path.search // "" - path.hash // "" - } - ``` - - @category Hooks + * Resolves the pathname of the given `to` value against the current location. Similar to {@link useHref}, but returns a {@link Path} instead of a string. + * + * @example + * import { useResolvedPath } from "react-router"; + * + * function SomeComponent() { + * // if the user is at /dashboard/profile + * let path = useResolvedPath("../accounts"); + * path.pathname; // "/dashboard/accounts" + * path.search; // "" + * path.hash; // "" + * } + * + * @public + * @category Hooks + * @param to The path to resolve + * @param options Options + * @param options.relative Defaults to "route" so routing is relative to the route tree. + * Set to "path" to make relative routing operate against path segments. + * @returns The resolved `Path` object with pathname, search, and hash */ export function useResolvedPath( to: To, @@ -388,35 +572,37 @@ export function useResolvedPath( } /** - Hook version of {@link Routes | ``} that uses objects instead of components. These objects have the same properties as the component props. - - The return value of `useRoutes` is either a valid React element you can use to render the route tree, or `null` if nothing matched. - - ```tsx - import * as React from "react"; - import { useRoutes } from "react-router"; - - function App() { - let element = useRoutes([ - { - path: "/", - element: , - children: [ - { - path: "messages", - element: , - }, - { path: "tasks", element: }, - ], - }, - { path: "team", element: }, - ]); - - return element; - } - ``` - - @category Hooks + * Hook version of {@link Routes | ``} that uses objects instead of components. These objects have the same properties as the component props. + * The return value of `useRoutes` is either a valid React element you can use to render the route tree, or `null` if nothing matched. + * + * @example + * import * as React from "react"; + * import { useRoutes } from "react-router"; + * + * function App() { + * let element = useRoutes([ + * { + * path: "/", + * element: , + * children: [ + * { + * path: "messages", + * element: , + * }, + * { path: "tasks", element: }, + * ], + * }, + * { path: "team", element: }, + * ]); + * + * return element; + * } + * + * @public + * @category Hooks + * @param routes An array of route objects that define the route hierarchy + * @param locationArg An optional location object or pathname string to use instead of the current location + * @returns A React element to render the matched route, or `null` if no routes matched */ export function useRoutes( routes: RouteObject[], @@ -425,12 +611,7 @@ export function useRoutes( return useRoutesImpl(routes, locationArg); } -/** - * Internal implementation with accept optional param for RouterProvider usage - * - * @private - * @category Hooks - */ +// Internal implementation with accept optional param for RouterProvider usage export function useRoutesImpl( routes: RouteObject[], locationArg?: Partial | string, @@ -982,56 +1163,65 @@ function useCurrentRouteId(hookName: DataRouterStateHook) { /** * Returns the ID for the nearest contextual route + * + * @category Hooks + * @returns The ID of the nearest contextual route */ export function useRouteId() { return useCurrentRouteId(DataRouterStateHook.UseRouteId); } /** - Returns the current navigation, defaulting to an "idle" navigation when no navigation is in progress. You can use this to render pending UI (like a global spinner) or read FormData from a form navigation. - - ```tsx - import { useNavigation } from "react-router" - - function SomeComponent() { - let navigation = useNavigation(); - navigation.state - navigation.formData - // etc. - } - ``` - - @category Hooks + * Returns the current navigation, defaulting to an "idle" navigation when no navigation is in progress. You can use this to render pending UI (like a global spinner) or read FormData from a form navigation. + * + * @example + * import { useNavigation } from "react-router"; + * + * function SomeComponent() { + * let navigation = useNavigation(); + * navigation.state; + * navigation.formData; + * // etc. + * } + * + * @public + * @category Hooks + * @mode framework + * @mode data + * @returns The current navigation object */ -export function useNavigation() { +export function useNavigation(): Navigation { let state = useDataRouterState(DataRouterStateHook.UseNavigation); return state.navigation; } /** - Revalidate the data on the page for reasons outside of normal data mutations like window focus or polling on an interval. - - ```tsx - import { useRevalidator } from "react-router"; - - function WindowFocusRevalidator() { - const revalidator = useRevalidator(); - - useFakeWindowFocus(() => { - revalidator.revalidate(); - }); - - return ( - - ); - } - ``` - - Note that page data is already revalidated automatically after actions. If you find yourself using this for normal CRUD operations on your data in response to user interactions, you're probably not taking advantage of the other APIs like {@link useFetcher}, {@link Form}, {@link useSubmit} that do this automatically. - - @category Hooks + * Revalidate the data on the page for reasons outside of normal data mutations like window focus or polling on an interval. + * + * @example + * import { useRevalidator } from "react-router"; + * + * function WindowFocusRevalidator() { + * const revalidator = useRevalidator(); + * + * useFakeWindowFocus(() => { + * revalidator.revalidate(); + * }); + * + * return ( + * + * ); + * } + * + * Note that page data is already revalidated automatically after actions. If you find yourself using this for normal CRUD operations on your data in response to user interactions, you're probably not taking advantage of the other APIs like {@link useFetcher}, {@link Form}, {@link useSubmit} that do this automatically. + * + * @public + * @category Hooks + * @mode framework + * @mode data + * @returns An object with a `revalidate` function and the current revalidation `state` */ export function useRevalidator(): { revalidate: () => Promise; @@ -1053,7 +1243,11 @@ export function useRevalidator(): { * Returns the active route matches, useful for accessing loaderData for * parent/child routes or the route "handle" property * + * @public * @category Hooks + * @mode framework + * @mode data + * @returns An array of UI matches for the current route hierarchy */ export function useMatches(): UIMatch[] { let { matches, loaderData } = useDataRouterState( @@ -1066,22 +1260,27 @@ export function useMatches(): UIMatch[] { } /** - Returns the data from the closest route {@link LoaderFunction | loader} or {@link ClientLoaderFunction | client loader}. - - ```tsx - import { useLoaderData } from "react-router" - - export async function loader() { - return await fakeDb.invoices.findAll(); - } - - export default function Invoices() { - let invoices = useLoaderData(); - // ... - } - ``` - - @category Hooks + * Returns the data from the closest route + * [`loader`](../../start/framework/route-module#loader) or + * [`clientLoader`](../../start/framework/route-module#clientloader). + * + * @example + * import { useLoaderData } from "react-router"; + * + * export async function loader() { + * return await fakeDb.invoices.findAll(); + * } + * + * export default function Invoices() { + * let invoices = useLoaderData(); + * // ... + * } + * + * @public + * @category Hooks + * @mode framework + * @mode data + * @returns The data returned from the route's loader function */ export function useLoaderData(): SerializeFrom { let state = useDataRouterState(DataRouterStateHook.UseLoaderData); @@ -1090,31 +1289,34 @@ export function useLoaderData(): SerializeFrom { } /** - Returns the loader data for a given route by route ID. - - ```tsx - import { useRouteLoaderData } from "react-router"; - - function SomeComponent() { - const { user } = useRouteLoaderData("root"); - } - ``` - - Route IDs are created automatically. They are simply the path of the route file relative to the app folder without the extension. - - | Route Filename | Route ID | - | -------------------------- | -------------------- | - | `app/root.tsx` | `"root"` | - | `app/routes/teams.tsx` | `"routes/teams"` | - | `app/whatever/teams.$id.tsx` | `"whatever/teams.$id"` | - - If you created an ID manually, you can use that instead: - - ```tsx - route("/", "containers/app.tsx", { id: "app" }}) - ``` - - @category Hooks + * Returns the loader data for a given route by route ID. + * + * Route IDs are created automatically. They are simply the path of the route file + * relative to the app folder without the extension. + * + * | Route Filename | Route ID | + * | ---------------------------- | ---------------------- | + * | `app/root.tsx` | `"root"` | + * | `app/routes/teams.tsx` | `"routes/teams"` | + * | `app/whatever/teams.$id.tsx` | `"whatever/teams.$id"` | + * + * @example + * import { useRouteLoaderData } from "react-router"; + * + * function SomeComponent() { + * const { user } = useRouteLoaderData("root"); + * } + * + * // You can also specify your own route ID's manually in your routes.ts file: + * route("/", "containers/app.tsx", { id: "app" }}) + * useRouteLoaderData("app"); + * + * @public + * @category Hooks + * @mode framework + * @mode data + * @param routeId The ID of the route to return loader data from + * @returns The data returned from the specified route's loader function, or undefined if not found */ export function useRouteLoaderData( routeId: string @@ -1124,29 +1326,32 @@ export function useRouteLoaderData( } /** - Returns the action data from the most recent POST navigation form submission or `undefined` if there hasn't been one. - - ```tsx - import { Form, useActionData } from "react-router" - - export async function action({ request }) { - const body = await request.formData() - const name = body.get("visitorsName") - return { message: `Hello, ${name}` } - } - - export default function Invoices() { - const data = useActionData() - return ( -
- - {data ? data.message : "Waiting..."} -
- ) - } - ``` - - @category Hooks + * Returns the action data from the most recent POST navigation form submission or `undefined` if there hasn't been one. + * + * @example + * import { Form, useActionData } from "react-router"; + * + * export async function action({ request }) { + * const body = await request.formData(); + * const name = body.get("visitorsName"); + * return { message: `Hello, ${name}` }; + * } + * + * export default function Invoices() { + * const data = useActionData(); + * return ( + *
+ * + * {data ? data.message : "Waiting..."} + *
+ * ); + * } + * + * @public + * @category Hooks + * @mode framework + * @mode data + * @returns The data returned from the route's action function, or undefined if no action has been called */ export function useActionData(): SerializeFrom | undefined { let state = useDataRouterState(DataRouterStateHook.UseActionData); @@ -1157,16 +1362,23 @@ export function useActionData(): SerializeFrom | undefined { } /** - Accesses the error thrown during an {@link ActionFunction | action}, {@link LoaderFunction | loader}, or component render to be used in a route module Error Boundary. - - ```tsx - export function ErrorBoundary() { - const error = useRouteError(); - return
{error.message}
; - } - ``` - - @category Hooks + * Accesses the error thrown during an + * [`action`](../../start/framework/route-module#action), + * [`loader`](../../start/framework/route-module#loader), + * or component render to be used in a route module + * [`ErrorBoundary`](../../start/framework/route-module#errorboundary). + * + * @example + * export function ErrorBoundary() { + * const error = useRouteError(); + * return
{error.message}
; + * } + * + * @public + * @category Hooks + * @mode framework + * @mode data + * @returns The error that was thrown during route loading, action execution, or rendering */ export function useRouteError(): unknown { let error = React.useContext(RouteErrorContext); @@ -1184,21 +1396,24 @@ export function useRouteError(): unknown { } /** - Returns the resolved promise value from the closest {@link Await | ``}. - - ```tsx - function SomeDescendant() { - const value = useAsyncValue(); - // ... - } - - // somewhere in your app - - - - ``` - - @category Hooks + * Returns the resolved promise value from the closest {@link Await | ``}. + * + * @example + * function SomeDescendant() { + * const value = useAsyncValue(); + * // ... + * } + * + * // somewhere in your app + * + * + * ; + * + * @public + * @category Hooks + * @mode framework + * @mode data + * @returns The resolved value from the nearest Await component */ export function useAsyncValue(): unknown { let value = React.useContext(AwaitContext); @@ -1206,26 +1421,29 @@ export function useAsyncValue(): unknown { } /** - Returns the rejection value from the closest {@link Await | ``}. - - ```tsx - import { Await, useAsyncError } from "react-router" - - function ErrorElement() { - const error = useAsyncError(); - return ( -

Uh Oh, something went wrong! {error.message}

- ); - } - - // somewhere in your app - } - /> - ``` - - @category Hooks + * Returns the rejection value from the closest {@link Await | ``}. + * + * @example + * import { Await, useAsyncError } from "react-router"; + * + * function ErrorElement() { + * const error = useAsyncError(); + * return ( + *

Uh Oh, something went wrong! {error.message}

+ * ); + * } + * + * // somewhere in your app + * } + * />; + * + * @public + * @category Hooks + * @mode framework + * @mode data + * @returns The error that was thrown in the nearest Await component */ export function useAsyncError(): unknown { let value = React.useContext(AwaitContext); @@ -1240,7 +1458,104 @@ let blockerId = 0; * using half-filled form data. This does not handle hard-reloads or * cross-origin navigations. * + * The Blocker object returned by the hook has the following properties: + * + * - **`state`** + * - `unblocked` - the blocker is idle and has not prevented any navigation + * - `blocked` - the blocker has prevented a navigation + * - `proceeding` - the blocker is proceeding through from a blocked navigation + * - **`location`** + * - When in a `blocked` state, this represents the {@link Location} to which we + * blocked a navigation. When in a `proceeding` state, this is the location + * being navigated to after a `blocker.proceed()` call. + * - **`proceed()`** + * - When in a `blocked` state, you may call `blocker.proceed()` to proceed to the + * blocked location. + * - **`reset()`** + * - When in a `blocked` state, you may call `blocker.reset()` to return the blocker + * back to an `unblocked` state and leave the user at the current location. + * + * @example + * // Boolean version + * const blocker = useBlocker(value !== ""); + * + * // Function version + * const blocker = useBlocker( + * ({ currentLocation, nextLocation, historyAction }) => + * value !== "" && + * currentLocation.pathname !== nextLocation.pathname + * ); + * + * @additionalExamples + * ```tsx + * import { useCallback, useState } from "react"; + * import { BlockerFunction, useBlocker } from "react-router"; + * + * export function ImportantForm() { + * const [value, setValue] = useState(""); + * + * const shouldBlock = useCallback( + * () => value !== "", + * [value] + * ); + * const blocker = useBlocker(shouldBlock); + * + * return ( + *
{ + * e.preventDefault(); + * setValue(""); + * if (blocker.state === "blocked") { + * blocker.proceed(); + * } + * }} + * > + * setValue(e.target.value)} + * /> + * + * + * + * {blocker.state === "blocked" ? ( + * <> + *

+ * Blocked the last navigation to + *

+ * + * + * + * ) : blocker.state === "proceeding" ? ( + *

+ * Proceeding through blocked navigation + *

+ * ) : ( + *

+ * Blocker is currently unblocked + *

+ * )} + *
+ * ); + * } + * ``` + * + * @public * @category Hooks + * @mode framework + * @mode data + * @param shouldBlock Either a boolean or a function returning a boolean which indicates whether the navigation should be blocked. The function format receives a single object parameter containing the `currentLocation`, `nextLocation`, and `historyAction` of the potential navigation. + * @returns A blocker object with state and reset functionality */ export function useBlocker(shouldBlock: boolean | BlockerFunction): Blocker { let { router, basename } = useDataRouterContext(DataRouterHook.UseBlocker); @@ -1304,12 +1619,8 @@ export function useBlocker(shouldBlock: boolean | BlockerFunction): Blocker { : IDLE_BLOCKER; } -/** - * Stable version of useNavigate that is used when we are in the context of - * a RouterProvider. - * - * @private - */ +// Stable version of useNavigate that is used when we are in the context of +// a RouterProvider. function useNavigateStable(): NavigateFunction { let { router } = useDataRouterContext(DataRouterHook.UseNavigateStable); let id = useCurrentRouteId(DataRouterStateHook.UseNavigateStable); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fe989f0d97..72e9bf517e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,6 +81,9 @@ importers: chalk: specifier: ^4.1.2 version: 4.1.2 + dox: + specifier: ^1.0.0 + version: 1.0.0 eslint: specifier: ^8.57.0 version: 8.57.0 @@ -96,6 +99,9 @@ importers: eslint-plugin-jest: specifier: ^27.9.0 version: 27.9.0(@typescript-eslint/eslint-plugin@7.5.0(@typescript-eslint/parser@7.5.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(jest@29.7.0(@types/node@22.14.0)(babel-plugin-macros@3.1.0))(typescript@5.4.5) + eslint-plugin-jsdoc: + specifier: ^51.3.4 + version: 51.3.4(eslint@8.57.0) eslint-plugin-jsx-a11y: specifier: ^6.8.0 version: 6.8.0(eslint@8.57.0) @@ -105,6 +111,9 @@ importers: eslint-plugin-react-hooks: specifier: next version: 6.1.0-canary-a7a11657-20250708(eslint@8.57.0) + fast-glob: + specifier: 3.2.11 + version: 3.2.11 isbot: specifier: ^5.1.11 version: 5.1.11 @@ -2887,6 +2896,10 @@ packages: '@emotion/hash@0.9.1': resolution: {integrity: sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==} + '@es-joy/jsdoccomment@0.52.0': + resolution: {integrity: sha512-BXuN7BII+8AyNtn57euU2Yxo9yA/KUDNzrpXyi3pfqKmBhhysR6ZWOebFh3vyPoqA3/j1SOvGgucElMGwlXing==} + engines: {node: '>=20.11.0'} + '@esbuild/aix-ppc64@0.19.12': resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} engines: {node: '>=12'} @@ -4725,6 +4738,9 @@ packages: '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/express-serve-static-core@4.17.43': resolution: {integrity: sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==} @@ -5007,6 +5023,10 @@ packages: resolution: {integrity: sha512-tv5B4IHeAdhR7uS4+bf8Ov3k793VEVHd45viRRkehIUZxm0WF82VPiLgHzA/Xl4TGPg1ZD49vfxBKFPecD5/mg==} engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/types@8.36.0': + resolution: {integrity: sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@5.62.0': resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -5127,6 +5147,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -5185,6 +5210,10 @@ packages: resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==} engines: {node: '>= 6.0.0'} + are-docs-informative@0.0.2: + resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} + engines: {node: '>=14'} + arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} @@ -5628,6 +5657,14 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + commander@9.4.0: + resolution: {integrity: sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw==} + engines: {node: ^12.20.0 || >=14} + + comment-parser@1.4.1: + resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} + engines: {node: '>= 12.0.0'} + component-emitter@1.3.0: resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==} @@ -5981,6 +6018,10 @@ packages: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} engines: {node: '>=10'} + dox@1.0.0: + resolution: {integrity: sha512-y0borLgGiqcXigOItzeBvWEPtZ5tkKMZ7MTa/9xhVCUz6sU1quXTTvbJGOLFZAu/4/nlj2Ui02A/tLqQFBXo+w==} + hasBin: true + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -6025,6 +6066,10 @@ packages: resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} engines: {node: '>=8.6'} + entities@3.0.1: + resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==} + engines: {node: '>=0.12'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -6195,6 +6240,12 @@ packages: jest: optional: true + eslint-plugin-jsdoc@51.3.4: + resolution: {integrity: sha512-maz6qa95+sAjMr9m5oRyfejc+mnyQWsWSe9oyv9371bh4/T0kWOMryJNO4h8rEd97wo/9lbzwi3OOX4rDhnAzg==} + engines: {node: '>=20.11.0'} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + eslint-plugin-jsx-a11y@6.8.0: resolution: {integrity: sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==} engines: {node: '>=4.0'} @@ -6241,12 +6292,20 @@ packages: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint@8.57.0: resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + espree@9.6.1: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -6260,6 +6319,10 @@ packages: resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} engines: {node: '>=0.10'} + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -7200,6 +7263,15 @@ packages: jsbn@1.1.0: resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + jsdoc-type-pratt-parser@4.1.0: + resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==} + engines: {node: '>=12.0.0'} + + jsdoctypeparser@9.0.0: + resolution: {integrity: sha512-jrTA2jJIL6/DAEILBEh2/w9QxCuwmvNXIry39Ay/HVfhE3o2yVV0U44blYkqdHA/OKloJEqvJy0xU+GSdE2SIw==} + engines: {node: '>=10'} + hasBin: true + jsdom@22.1.0: resolution: {integrity: sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==} engines: {node: '>=16'} @@ -7363,6 +7435,9 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + linkify-it@4.0.1: + resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==} + linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} @@ -7454,6 +7529,10 @@ packages: resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} engines: {node: '>=16'} + markdown-it@13.0.1: + resolution: {integrity: sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==} + hasBin: true + markdown-it@14.1.0: resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} hasBin: true @@ -7525,6 +7604,9 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdurl@1.0.1: + resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} + mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} @@ -8143,10 +8225,16 @@ packages: parse-entities@4.0.1: resolution: {integrity: sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==} + parse-imports-exports@0.2.4: + resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse-statements@1.0.11: + resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==} + parse5-htmlparser2-tree-adapter@7.0.0: resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} @@ -9006,6 +9094,9 @@ packages: spdx-expression-parse@3.0.1: resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + spdx-expression-parse@4.0.0: + resolution: {integrity: sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==} + spdx-license-ids@3.0.12: resolution: {integrity: sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==} @@ -9379,6 +9470,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uc.micro@1.0.6: + resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} + uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} @@ -11127,6 +11221,14 @@ snapshots: '@emotion/hash@0.9.1': {} + '@es-joy/jsdoccomment@0.52.0': + dependencies: + '@types/estree': 1.0.8 + '@typescript-eslint/types': 8.36.0 + comment-parser: 1.4.1 + esquery: 1.6.0 + jsdoc-type-pratt-parser: 4.1.0 + '@esbuild/aix-ppc64@0.19.12': optional: true @@ -13023,6 +13125,8 @@ snapshots: '@types/estree@1.0.7': {} + '@types/estree@1.0.8': {} + '@types/express-serve-static-core@4.17.43': dependencies: '@types/node': 22.14.0 @@ -13364,6 +13468,8 @@ snapshots: '@typescript-eslint/types@7.5.0': {} + '@typescript-eslint/types@8.36.0': {} + '@typescript-eslint/typescript-estree@5.62.0(typescript@5.4.5)': dependencies: '@typescript-eslint/types': 5.62.0 @@ -13630,12 +13736,18 @@ snapshots: dependencies: acorn: 8.14.1 + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn-walk@8.3.2: {} acorn@8.14.0: {} acorn@8.14.1: {} + acorn@8.15.0: {} + agent-base@6.0.2: dependencies: debug: 4.4.1 @@ -13690,6 +13802,8 @@ snapshots: app-root-path@3.1.0: {} + are-docs-informative@0.0.2: {} + arg@5.0.2: {} argparse@1.0.10: @@ -14264,6 +14378,10 @@ snapshots: commander@4.1.1: {} + commander@9.4.0: {} + + comment-parser@1.4.1: {} + component-emitter@1.3.0: {} compressible@2.0.18: @@ -14580,6 +14698,12 @@ snapshots: dotenv@8.6.0: {} + dox@1.0.0: + dependencies: + commander: 9.4.0 + jsdoctypeparser: 9.0.0 + markdown-it: 13.0.1 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -14622,6 +14746,8 @@ snapshots: dependencies: ansi-colors: 4.1.3 + entities@3.0.1: {} + entities@4.5.0: {} err-code@2.0.3: {} @@ -14977,6 +15103,22 @@ snapshots: - supports-color - typescript + eslint-plugin-jsdoc@51.3.4(eslint@8.57.0): + dependencies: + '@es-joy/jsdoccomment': 0.52.0 + are-docs-informative: 0.0.2 + comment-parser: 1.4.1 + debug: 4.4.1 + escape-string-regexp: 4.0.0 + eslint: 8.57.0 + espree: 10.4.0 + esquery: 1.6.0 + parse-imports-exports: 0.2.4 + semver: 7.7.2 + spdx-expression-parse: 4.0.0 + transitivePeerDependencies: + - supports-color + eslint-plugin-jsx-a11y@6.8.0(eslint@8.57.0): dependencies: '@babel/runtime': 7.24.1 @@ -15057,6 +15199,8 @@ snapshots: eslint-visitor-keys@3.4.3: {} + eslint-visitor-keys@4.2.1: {} + eslint@8.57.0: dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) @@ -15100,6 +15244,12 @@ snapshots: transitivePeerDependencies: - supports-color + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + espree@9.6.1: dependencies: acorn: 8.14.1 @@ -15112,6 +15262,10 @@ snapshots: dependencies: estraverse: 5.3.0 + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 @@ -16361,6 +16515,10 @@ snapshots: jsbn@1.1.0: {} + jsdoc-type-pratt-parser@4.1.0: {} + + jsdoctypeparser@9.0.0: {} + jsdom@22.1.0: dependencies: abab: 2.0.6 @@ -16516,6 +16674,10 @@ snapshots: lines-and-columns@1.2.4: {} + linkify-it@4.0.1: + dependencies: + uc.micro: 1.0.6 + linkify-it@5.0.0: dependencies: uc.micro: 2.1.0 @@ -16611,6 +16773,14 @@ snapshots: markdown-extensions@2.0.0: {} + markdown-it@13.0.1: + dependencies: + argparse: 2.0.1 + entities: 3.0.1 + linkify-it: 4.0.1 + mdurl: 1.0.1 + uc.micro: 1.0.6 + markdown-it@14.1.0: dependencies: argparse: 2.0.1 @@ -16811,6 +16981,8 @@ snapshots: dependencies: '@types/mdast': 4.0.3 + mdurl@1.0.1: {} + mdurl@2.0.0: {} media-query-parser@2.0.2: @@ -17769,6 +17941,10 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 + parse-imports-exports@0.2.4: + dependencies: + parse-statements: 1.0.11 + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.27.1 @@ -17776,6 +17952,8 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse-statements@1.0.11: {} + parse5-htmlparser2-tree-adapter@7.0.0: dependencies: domhandler: 5.0.3 @@ -18701,6 +18879,11 @@ snapshots: spdx-exceptions: 2.3.0 spdx-license-ids: 3.0.12 + spdx-expression-parse@4.0.0: + dependencies: + spdx-exceptions: 2.3.0 + spdx-license-ids: 3.0.12 + spdx-license-ids@3.0.12: {} sprintf-js@1.0.3: {} @@ -18920,7 +19103,7 @@ snapshots: terser@5.15.0: dependencies: '@jridgewell/source-map': 0.3.6 - acorn: 8.14.1 + acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 optional: true @@ -19142,6 +19325,8 @@ snapshots: typescript@5.4.5: {} + uc.micro@1.0.6: {} + uc.micro@2.1.0: {} ufo@1.6.1: {} diff --git a/scripts/docs.ts b/scripts/docs.ts new file mode 100644 index 0000000000..3d0c96c657 --- /dev/null +++ b/scripts/docs.ts @@ -0,0 +1,551 @@ +import fs from "node:fs"; +import path from "node:path"; +import util from "node:util"; +import fg from "fast-glob"; + +import dox from "dox"; +import { ReflectionKind, type JSONOutput } from "typedoc"; +import ts from "typescript"; + +type UnknownTag = { + type: string; + string: string; + html: string; +}; + +type ParamTag = { + type: "param"; + string: string; + name: string; + description: string; + // TODO: Are these used? + types: []; + typesDescription: ""; + // end + variable: boolean; + nonNullable: boolean; + nullable: boolean; + optional: boolean; +}; + +type Tag = ParamTag | UnknownTag; + +type ParsedComment = { + tags: Tag[]; + description: { + full: string; + summary: string; + body: string; + }; + isPrivate: boolean; + isConstructor: boolean; + isClass: boolean; + isEvent: boolean; + ignore: boolean; + line: number; + codeStart: number; + code: string; + ctx: { + type: string; + name: string; + string: string; + }; +}; + +export type GetArrayElementType = + T extends readonly (infer U)[] ? U : never; + +type Mode = GetArrayElementType; +type Category = GetArrayElementType; + +type SimplifiedComment = { + category: Category; + name: string; + // TODO: Allow modes on different props + modes: Mode[]; + summary: string; + example?: string; + additionalExamples?: string; + reference?: string; + signature: string; + params: { + name: string; + description: string; + }[]; + returns: string; +}; + +const MODES = ["framework", "data", "declarative"] as const; +const CATEGORIES = [ + "components", + "hooks", + "data routers", + "declarative routers", + "utils", +] as const; + +// Read a filename from standard input using the node parseArgs utility + +const { values: args } = util.parseArgs({ + args: process.argv.slice(2), + options: { + path: { + type: "string", + short: "p", + }, + api: { + type: "string", + short: "a", + }, + write: { + type: "boolean", + short: "w", + }, + output: { + type: "string", + short: "o", + }, + }, + allowPositionals: true, +}); + +if (!args.path) { + console.error( + "Usage: docs.ts --path [--api ] [--write] [--output ]" + ); + console.error(" --path, -p File path or glob pattern to parse"); + console.error( + " --api, -a Comma-separated list of specific APIs to generate" + ); + console.error(" --write, -w Write markdown files to output directory"); + console.error(" --output, -o Output directory (default: docs/api)"); + process.exit(1); +} + +// Parse the API filter if provided +let apiFilter: string[] | null = null; +if (args.api) { + apiFilter = args.api.split(",").map((name) => name.trim()); +} + +// Configure output directory +const outputDir = args.output || "docs/api"; + +// Build lookup table for @link resolution +const repoApiLookup = buildRepoDocsLookupTable(outputDir); +const typedocLookup = buildTypedocLookupTable(outputDir); + +// Resolve file paths using glob patterns +const filePaths = fg.sync(args.path, { + onlyFiles: true, + ignore: ["**/node_modules/**", "**/__tests__/**", "**/dist/**"], +}); + +if (filePaths.length === 0) { + console.error(`No files found matching pattern: ${args.path}`); + process.exit(1); +} + +// Generate markdown documentation for all matching files +filePaths.forEach((filePath) => { + console.log(`\nProcessing file: ${filePath}`); + generateMarkdownDocs(filePath, apiFilter, outputDir, args.write); +}); + +function buildRepoDocsLookupTable(outputDir: string): Map { + const lookup = new Map(); + + // Add existing files if output directory exists + if (!fs.existsSync(outputDir)) { + throw new Error( + `Docs directory does not exist for cross-linking: ${outputDir}` + ); + } + + const markdownFiles = fg.sync(`${outputDir}/**/*.md`, { + onlyFiles: true, + }); + + markdownFiles.forEach((filePath) => { + const relativePath = path + .relative(outputDir, filePath) + .replace(/\.md$/, ""); + const apiName = path.basename(relativePath); + + if (apiName !== "index") { + lookup.set(apiName, relativePath); + } + }); + + return lookup; +} +function buildTypedocLookupTable(outputDir: string): Map { + const lookup = new Map(); + + // Prerequisite: `typedoc` has been run first via `npm run docs` + if (fs.existsSync("public/dev/api.json")) { + let apiData = JSON.parse( + fs.readFileSync("public/dev/api.json", "utf8") + ) as JSONOutput.ProjectReflection; + + apiData.children + ?.filter((c) => c.kind === ReflectionKind.Module) + .forEach((child) => processTypedocModule(child, lookup)); + } else { + console.warn( + '⚠️ Typedoc API data not found at "public/dev/api.json", will not automatically cross-link to Reference Docs' + ); + } + + return lookup; +} + +function processTypedocModule( + child: JSONOutput.ReferenceReflection | JSONOutput.DeclarationReflection, + lookup: Map, + prefix: string[] = [] +) { + let newPrefix = [...prefix, child.name]; + let moduleName = newPrefix.join("."); + child.children?.forEach((subChild) => { + // Recurse into submodules + if (subChild.kind === ReflectionKind.Module) { + processTypedocModule(subChild, lookup, newPrefix); + return; + } + + // Prefer linking to repo docs over typedoc docs + if (lookup.has(subChild.name)) { + return; + } + + let apiName = `${moduleName}.${subChild.name}`; + let type = + subChild.kind === ReflectionKind.Enum + ? "enums" + : subChild.kind === ReflectionKind.Class + ? "classes" + : subChild.kind === ReflectionKind.Interface + ? "interfaces" + : subChild.kind === ReflectionKind.TypeAlias + ? "types" + : subChild.kind === ReflectionKind.Function + ? "functions" + : subChild.kind === ReflectionKind.Variable + ? "variables" + : undefined; + + if (!type) { + console.warn( + `Skipping ${apiName} because it is not a function, class, enum, interface, or type` + ); + return; + } + let modulePath = moduleName.replace(/[@\-/]/g, "_"); + let path = `${type}/${modulePath}.${subChild.name}.html`; + let url = `https://api.reactrouter.com/v7/${path}`; + lookup.set(subChild.name, url); + }); +} + +function generateMarkdownDocs( + filepath: string, + apiFilter?: string[] | null, + outputDir?: string, + writeFiles?: boolean +) { + let data = parseDocComments(filepath); + + data.forEach((comment) => { + // Skip if API filter is provided and this API is not in the filter + if (apiFilter && !apiFilter.includes(comment.name)) { + return; + } + + // Generate markdown content for each public function + let markdownContent = generateMarkdownForComment(comment); + if (markdownContent) { + if (writeFiles && outputDir) { + // Write to file based on category + writeMarkdownFile(comment, markdownContent, outputDir); + } else { + // Print to console (existing behavior) + console.log(`\n=== Markdown for ${comment.name} ===`); + console.log(markdownContent); + console.log(`=== End of ${comment.name} ===\n`); + } + } + }); +} + +function writeMarkdownFile( + comment: SimplifiedComment, + markdownContent: string, + outputDir: string +) { + // Convert category to lowercase and replace spaces with hyphens for folder name + const categoryFolder = comment.category.toLowerCase().replace(/\s+/g, "-"); + + // Create the full directory path + const targetDir = path.join(outputDir, categoryFolder); + + // Ensure the directory exists + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }); + } + + // Create the filename (e.g., useHref.md) + const filename = `${comment.name}.md`; + const filePath = path.join(targetDir, filename); + + // Write the file + fs.writeFileSync(filePath, markdownContent, "utf8"); + console.log(`✓ Written: ${filePath}`); +} + +function generateMarkdownForComment(comment: SimplifiedComment): string { + let markdown = ""; + + // Skip functions without proper names + if (!comment.name || comment.name === "undefined") { + return ""; + } + + // Title with frontmatter + markdown += `---\n`; + markdown += `title: ${comment.name}\n`; + markdown += `---\n\n`; + + markdown += `# ${comment.name}\n\n`; + + markdown += `\n\n`; + + // Modes section + if (comment.modes && comment.modes.length > 0) { + markdown += `[MODES: ${comment.modes.join(", ")}]\n\n`; + } + + // Summary section + markdown += `## Summary\n\n`; + + // Generate reference documentation link from @reference tag or fallback to default + if (comment.reference) { + markdown += `[Reference Documentation ↗](${comment.reference})\n\n`; + } + + // Clean up HTML tags from summary and convert to plain text + let summary = resolveLinkTags(comment.summary); + markdown += `${summary}\n\n`; + + // Example section (if available) + if (comment.example) { + let example = resolveLinkTags(comment.example); + markdown += `\`\`\`tsx\n${example}\n\`\`\`\n\n`; + } + + // Signature section + markdown += `## Signature\n\n`; + markdown += "```tsx\n"; + markdown += `${comment.signature}\n`; + markdown += "```\n\n"; + + // Parameters section + if (comment.params && comment.params.length > 0) { + markdown += `## Params\n\n`; + comment.params.forEach((param, i) => { + // Only show modes for parameters if they differ from hook-level modes + // For now, we assume all parameters have the same modes as the hook + // This could be enhanced in the future if we need per-parameter mode support + + // Clean up HTML tags from description + let description = resolveLinkTags(param.description); + + // Skip options object param that is there for JSDoc since we will document each option on it own + if (param.name === "options" && description === "Options") { + if (!comment.params[i + 1].name.startsWith("options.")) { + throw new Error( + "Expected docs for individual options: " + comment.name + ); + } + return; + } + + markdown += `### ${param.name}\n\n`; + markdown += `${description || "_No documentation_"}\n\n`; + }); + } + + // Additional Examples section (if available) + if (comment.additionalExamples) { + let additionalExamples = resolveLinkTags(comment.additionalExamples); + markdown += `## Examples\n\n`; + markdown += `${additionalExamples}\n\n`; + } + + return markdown; +} + +function parseDocComments(filepath: string): SimplifiedComment[] { + let code = fs.readFileSync(filepath).toString(); + let comments = dox.parseComments(code, { raw: true }) as ParsedComment[]; + return comments + .filter((c) => c.tags.some((t) => t.type === "public")) + .map((c) => simplifyComment(c)); +} + +function simplifyComment(comment: ParsedComment): SimplifiedComment { + let name = comment.ctx.name; + if (!name) { + let matches = comment.code.match(/function ([^<(]+)/); + if (matches) { + name = matches[1]; + } + if (!name) { + throw new Error(`Could not determine API name:\n${comment.code}\n`); + } + } + + let categoryTags = comment.tags.filter((t) => t.type === "category"); + if (categoryTags.length !== 1) { + throw new Error(`Expected a single category tag: ${name}`); + } + let category = categoryTags[0].string as Category; + + let modes: Mode[] = [...MODES]; + let modeTags = comment.tags.filter((t) => t.type === "mode"); + if (modeTags.length > 0) { + modes = modeTags.map((mode) => mode.string as Mode); + } + + let summary = comment.description.full; + if (!summary) { + throw new Error(`Expected a summary: ${name}`); + } + + let example = comment.tags.find((t) => t.type === "example")?.string; + let additionalExamples = comment.tags.find( + (t) => t.type === "additionalExamples" + )?.string; + + let reference = typedocLookup.get(name); + if (!reference) { + throw new Error(`Could not find API in typedoc reference docs: ${name}`); + } + + let signature = getSignature(comment.code); + + let params: SimplifiedComment["params"] = []; + comment.tags.forEach((tag) => { + if (isParamTag(tag)) { + params.push({ + name: tag.name, + description: tag.description, + }); + } + }); + + let returns = comment.tags.find((t) => t.type === "returns")?.string; + if (!returns) { + throw new Error(`Expected a @returns tag: ${name}`); + } + + return { + category, + name, + modes, + summary, + example, + additionalExamples, + reference, + signature, + params, + returns, + }; +} + +function isParamTag(tag: Tag): tag is ParamTag { + return tag.type === "param"; +} + +// Parse the TypeScript code into an AST so we can remove the function body +// and just grab the signature +function getSignature(code: string): string { + const ast = ts.createSourceFile("example.ts", code, ts.ScriptTarget.Latest); + if (ast.statements.length === 0) { + throw new Error(`Expected one or more statements: ${code}`); + } + + let functionDeclaration = ast.statements[0]; + if (!ts.isFunctionDeclaration(functionDeclaration)) { + throw new Error(`Expected a function declaration: ${code}`); + } + + let modifiedFunction = { + ...functionDeclaration, + modifiers: functionDeclaration.modifiers?.filter( + (m) => m.kind !== ts.SyntaxKind.ExportKeyword + ), + body: ts.factory.createBlock([], false), + } as ts.FunctionDeclaration; + + let newCode = ts + .createPrinter({ newLine: ts.NewLineKind.LineFeed }) + .printNode(ts.EmitHint.Unspecified, modifiedFunction, ast); + + return newCode + .replace(/^function /, "") + .replace("{ }", "") + .trim(); +} + +/** + * Resolves {@link ...} tags in JSDoc text and converts them to markdown links + * @param text - The text containing {@link ...} tags + * @returns Text with {@link ...} tags replaced by markdown links + */ +function resolveLinkTags(text: string): string { + // Match {@link ApiName} or {@link ApiName description} + const linkPattern = /\{@link\s+([^}]+)\}/g; + + return text.replace(linkPattern, (match, linkContent) => { + const parts = linkContent + .replace("@link", "") + .trim() + .split("|") + .map((p) => p.trim()); + const apiName = parts[0]; + const description = parts[1] || `\`${apiName}\``; + + // Look up the API in the lookup table + let relativePath; + if (repoApiLookup.has(apiName)) { + relativePath = repoApiLookup.get(apiName); + } else if (typedocLookup.has(apiName)) { + console.log( + `Could not find markdown doc, resolved from typedoc docs: {@link ${apiName}}` + ); + relativePath = typedocLookup.get(apiName); + } + + if (relativePath) { + // Convert to markdown link + let href = relativePath.startsWith("http") + ? relativePath + : `../${relativePath}`; + return `[${description}](${href})`; + } else { + // If not found, return as plain text with a warning + console.warn( + `Warning: Could not resolve {@link ${apiName}} in documentation` + ); + return description; + } + }); +} From 7637e065a15095d5563cb875d6a9015272e1b94d Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Fri, 11 Jul 2025 20:19:55 +0000 Subject: [PATCH 02/61] chore: deduplicate `pnpm-lock.yaml` --- pnpm-lock.yaml | 71 ++++++++++++++++++-------------------------------- 1 file changed, 26 insertions(+), 45 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72e9bf517e..e8b724c275 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,7 +110,7 @@ importers: version: 7.34.1(eslint@8.57.0) eslint-plugin-react-hooks: specifier: next - version: 6.1.0-canary-a7a11657-20250708(eslint@8.57.0) + version: 6.1.0-canary-97cdd5d3-20250710(eslint@8.57.0) fast-glob: specifier: 3.2.11 version: 3.2.11 @@ -5142,11 +5142,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - acorn@8.14.1: - resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -6258,8 +6253,8 @@ packages: peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 - eslint-plugin-react-hooks@6.1.0-canary-a7a11657-20250708: - resolution: {integrity: sha512-iAY0n014i7bIKWiex/vOB3CH/slI4tT8obRR5OysIuRpuTEVcfIVh1ZNtDIlHq39rbQeN139nyGqtE27erTUXA==} + eslint-plugin-react-hooks@6.1.0-canary-97cdd5d3-20250710: + resolution: {integrity: sha512-Gn9yHEDxDSBd4hrUltaX5gNBiwo5ea0QbCXu6g7QGb/Y28bIaRIG0WESgd2ztkQ9xBThcNxdINem4X2Dlt+naA==} engines: {node: '>=18'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 @@ -6315,10 +6310,6 @@ packages: engines: {node: '>=4'} hasBin: true - esquery@1.5.0: - resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} - engines: {node: '>=0.10'} - esquery@1.6.0: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} @@ -11845,7 +11836,7 @@ snapshots: '@mdx-js/mdx@3.0.1': dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 '@types/mdx': 2.0.12 @@ -12850,7 +12841,7 @@ snapshots: '@rollup/pluginutils@5.1.0(rollup@4.43.0)': dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 2.3.1 optionalDependencies: @@ -13065,7 +13056,7 @@ snapshots: '@types/acorn@4.0.6': dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/aria-query@5.0.4': {} @@ -13121,7 +13112,7 @@ snapshots: '@types/estree-jsx@1.0.5': dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/estree@1.0.7': {} @@ -13732,10 +13723,6 @@ snapshots: mime-types: 3.0.1 negotiator: 1.0.0 - acorn-jsx@5.3.2(acorn@8.14.1): - dependencies: - acorn: 8.14.1 - acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -13744,8 +13731,6 @@ snapshots: acorn@8.14.0: {} - acorn@8.14.1: {} - acorn@8.15.0: {} agent-base@6.0.2: @@ -15143,7 +15128,7 @@ snapshots: dependencies: eslint: 8.57.0 - eslint-plugin-react-hooks@6.1.0-canary-a7a11657-20250708(eslint@8.57.0): + eslint-plugin-react-hooks@6.1.0-canary-97cdd5d3-20250710(eslint@8.57.0): dependencies: '@babel/core': 7.27.7 '@babel/parser': 7.27.7 @@ -15220,7 +15205,7 @@ snapshots: eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 espree: 9.6.1 - esquery: 1.5.0 + esquery: 1.6.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 6.0.1 @@ -15252,16 +15237,12 @@ snapshots: espree@9.6.1: dependencies: - acorn: 8.14.1 - acorn-jsx: 5.3.2(acorn@8.14.1) + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) eslint-visitor-keys: 3.4.3 esprima@4.0.1: {} - esquery@1.5.0: - dependencies: - estraverse: 5.3.0 - esquery@1.6.0: dependencies: estraverse: 5.3.0 @@ -15276,7 +15257,7 @@ snapshots: estree-util-attach-comments@3.0.0: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 estree-util-build-jsx@3.0.1: dependencies: @@ -15302,7 +15283,7 @@ snapshots: estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 esutils@2.0.3: {} @@ -15755,7 +15736,7 @@ snapshots: hast-util-to-estree@3.1.0: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 comma-separated-tokens: 2.0.3 @@ -15790,7 +15771,7 @@ snapshots: hast-util-to-jsx-runtime@2.3.0: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/hast': 3.0.4 '@types/unist': 3.0.2 comma-separated-tokens: 2.0.3 @@ -16052,7 +16033,7 @@ snapshots: is-reference@3.0.2: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 is-regex@1.1.4: dependencies: @@ -17115,7 +17096,7 @@ snapshots: micromark-extension-mdx-expression@3.0.0: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 devlop: 1.1.0 micromark-factory-mdx-expression: 2.0.1 micromark-factory-space: 2.0.0 @@ -17127,7 +17108,7 @@ snapshots: micromark-extension-mdx-jsx@3.0.0: dependencies: '@types/acorn': 4.0.6 - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 micromark-factory-mdx-expression: 2.0.1 @@ -17143,7 +17124,7 @@ snapshots: micromark-extension-mdxjs-esm@3.0.0: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 devlop: 1.1.0 micromark-core-commonmark: 2.0.0 micromark-util-character: 2.1.0 @@ -17155,8 +17136,8 @@ snapshots: micromark-extension-mdxjs@3.0.0: dependencies: - acorn: 8.14.1 - acorn-jsx: 5.3.2(acorn@8.14.1) + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) micromark-extension-mdx-expression: 3.0.0 micromark-extension-mdx-jsx: 3.0.0 micromark-extension-mdx-md: 2.0.0 @@ -17192,7 +17173,7 @@ snapshots: micromark-factory-mdx-expression@2.0.1: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 devlop: 1.1.0 micromark-util-character: 2.1.0 micromark-util-events-to-acorn: 2.0.2 @@ -17308,7 +17289,7 @@ snapshots: micromark-util-events-to-acorn@2.0.2: dependencies: '@types/acorn': 4.0.6 - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/unist': 3.0.2 devlop: 1.1.0 estree-util-visit: 2.0.0 @@ -17508,7 +17489,7 @@ snapshots: mlly@1.7.4: dependencies: - acorn: 8.14.1 + acorn: 8.15.0 pathe: 2.0.3 pkg-types: 1.3.1 ufo: 1.6.1 @@ -18004,13 +17985,13 @@ snapshots: periscopic@3.1.0: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 estree-walker: 3.0.3 is-reference: 3.0.2 periscopic@4.0.2: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 is-reference: 3.0.2 zimmerframe: 1.1.2 From 52dbe5386309240adc08b5ceb9eccc4219a682cd Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Fri, 11 Jul 2025 20:22:49 +0000 Subject: [PATCH 03/61] chore: generate markdown docs from jsdocs --- docs/api/hooks/useActionData.md | 14 +++++++++++++- docs/api/hooks/useLoaderData.md | 18 ++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/docs/api/hooks/useActionData.md b/docs/api/hooks/useActionData.md index 98e1a91b3a..29c08b4cb6 100644 --- a/docs/api/hooks/useActionData.md +++ b/docs/api/hooks/useActionData.md @@ -4,6 +4,17 @@ title: useActionData # useActionData + + [MODES: framework, data] ## Summary @@ -35,5 +46,6 @@ export default function Invoices() { ## Signature ```tsx -useActionData(): undefined +useActionData(): SerializeFrom | undefined ``` + diff --git a/docs/api/hooks/useLoaderData.md b/docs/api/hooks/useLoaderData.md index 6ce520494e..b420dd74cf 100644 --- a/docs/api/hooks/useLoaderData.md +++ b/docs/api/hooks/useLoaderData.md @@ -4,13 +4,26 @@ title: useLoaderData # useLoaderData + + [MODES: framework, data] ## Summary [Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react_router.useLoaderData.html) -Returns the data from the closest route [LoaderFunction](https://api.reactrouter.com/v7/types/react_router.LoaderFunction.html) or [ClientLoaderFunction](https://api.reactrouter.com/v7/types/react_router.ClientLoaderFunction.html). +Returns the data from the closest route +[`loader`](../../start/framework/route-module#loader) or +[`clientLoader`](../../start/framework/route-module#clientloader). ```tsx import { useLoaderData } from "react-router"; @@ -28,5 +41,6 @@ export default function Invoices() { ## Signature ```tsx -useLoaderData(): SerializeFrom +useLoaderData(): SerializeFrom ``` + From b31f1e1104d03307c85b1668c94b8dfde5850ca3 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 11 Jul 2025 16:31:53 -0400 Subject: [PATCH 04/61] Run docs workflow on commits to dev --- .github/workflows/docs.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ad63e44b8d..50ee6cec89 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,10 +1,15 @@ name: 📚 Docs on: + push: + branches: + # Enable main after the next release beyond `7.7.0` + # - main + - dev workflow_dispatch: inputs: branch: - description: "Branch to generate docs for" + description: "Branch to generate docs for (usually dev)" required: true api: description: "API Names to generate docs for" From 4f9a9657c423251e00cb9f41dcc8fa9a9643aed2 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Fri, 11 Jul 2025 20:35:39 +0000 Subject: [PATCH 05/61] chore: generate markdown docs from jsdocs --- docs/api/hooks/useBlocker.md | 82 +++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/docs/api/hooks/useBlocker.md b/docs/api/hooks/useBlocker.md index 7192ba4515..5f0a84baaa 100644 --- a/docs/api/hooks/useBlocker.md +++ b/docs/api/hooks/useBlocker.md @@ -4,35 +4,50 @@ title: useBlocker # useBlocker + + [MODES: framework, data] ## Summary [Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react_router.useBlocker.html) -Allow the application to block navigations within the SPA and present the user a confirmation dialog to confirm the navigation. Mostly used to avoid using half-filled form data. This does not handle hard-reloads or cross-origin navigations. - -## Signature +Allow the application to block navigations within the SPA and present the +user a confirmation dialog to confirm the navigation. Mostly used to avoid +using half-filled form data. This does not handle hard-reloads or +cross-origin navigations. + +The Blocker object returned by the hook has the following properties: + +- **`state`** + - `unblocked` - the blocker is idle and has not prevented any navigation + - `blocked` - the blocker has prevented a navigation + - `proceeding` - the blocker is proceeding through from a blocked navigation +- **`location`** + - When in a `blocked` state, this represents the [`Location`](https://api.reactrouter.com/v7/interfaces/react_router.Location.html) to which we + blocked a navigation. When in a `proceeding` state, this is the location + being navigated to after a `blocker.proceed()` call. +- **`proceed()`** + - When in a `blocked` state, you may call `blocker.proceed()` to proceed to the + blocked location. +- **`reset()`** + - When in a `blocked` state, you may call `blocker.reset()` to return the blocker + back to an `unblocked` state and leave the user at the current location. ```tsx -useBlocker(shouldBlock: boolean | BlockerFunction): Blocker -``` - -## Params - -### shouldBlock - -[modes: framework, data] - -**boolean** - -Whether or not the navigation should be blocked. If `true`, the blocker will prevent the navigation. If `false`, the blocker will not prevent the navigation. - -[**BlockerFunction**](https://api.reactrouter.com/v7/types/react_router.BlockerFunction.html) +// Boolean version +const blocker = useBlocker(value !== ""); -A function that returns a boolean indicating whether the navigation should be blocked. - -```tsx +// Function version const blocker = useBlocker( ({ currentLocation, nextLocation, historyAction }) => value !== "" && @@ -40,32 +55,20 @@ const blocker = useBlocker( ); ``` -## Blocker - -The [Blocker](https://api.reactrouter.com/v7/types/react_router.Blocker.html) object returned by the hook. It has the following properties: - -### `state` - -- `unblocked` - the blocker is idle and has not prevented any navigation -- `blocked` - the blocker has prevented a navigation -- `proceeding` - the blocker is proceeding through from a blocked navigation - -### `location` - -When in a `blocked` state, this represents the [`Location`](https://api.reactrouter.com/v7/interfaces/react_router.Location.html) to which we blocked a navigation. When in a `proceeding` state, this is the location being navigated to after a `blocker.proceed()` call. +## Signature -### `proceed()` +```tsx +useBlocker(shouldBlock: boolean | BlockerFunction): Blocker +``` -When in a `blocked` state, you may call `blocker.proceed()` to proceed to the blocked location. +## Params -### `reset()` +### shouldBlock -When in a `blocked` state, you may call `blocker.reset()` to return the blocker back to an `unblocked` state and leave the user at the current location. +Either a boolean or a function returning a boolean which indicates whether the navigation should be blocked. The function format receives a single object parameter containing the `currentLocation`, `nextLocation`, and `historyAction` of the potential navigation. ## Examples -### Basic - ```tsx import { useCallback, useState } from "react"; import { BlockerFunction, useBlocker } from "react-router"; @@ -128,3 +131,4 @@ export function ImportantForm() { ); } ``` + From 6d447ce99e25cc1604b8740a1a4af10bb75e89bf Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Fri, 11 Jul 2025 16:40:00 -0400 Subject: [PATCH 06/61] Copy docs workflow from main --- .github/workflows/docs.yml | 73 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 .github/workflows/docs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000000..50ee6cec89 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,73 @@ +name: 📚 Docs + +on: + push: + branches: + # Enable main after the next release beyond `7.7.0` + # - main + - dev + workflow_dispatch: + inputs: + branch: + description: "Branch to generate docs for (usually dev)" + required: true + api: + description: "API Names to generate docs for" + required: false + default: "" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + docs: + if: github.repository == 'remix-run/react-router' + runs-on: ubuntu-latest + + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@v4 + with: + token: ${{ secrets.FORMAT_PAT }} + ref: ${{ github.event.inputs.branch }} + + - name: 📦 Setup pnpm + uses: pnpm/action-setup@v4 + + - name: ⎔ Setup node + uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + cache: pnpm + + - name: 📥 Install deps + run: pnpm install --frozen-lockfile + + - name: 🏗 Build + run: pnpm build + + - name: 📚 Generate Typedoc Docs + run: pnpm run docs + + - name: 📚 Generate Markdown Docs (for all APIs) + if: github.event.inputs.api == '' + run: node --experimental-strip-types scripts/docs.ts --path packages/react-router/lib/hooks.tsx --write + + - name: 📚 Generate Markdown Docs (for specific APIs) + if: github.event.inputs.api != '' + run: node --experimental-strip-types scripts/docs.ts --path packages/react-router/lib/hooks.tsx --write --api ${{ github.event.inputs.api }} + + - name: 💪 Commit + run: | + git config --local user.email "hello@remix.run" + git config --local user.name "Remix Run Bot" + + git add . + if [ -z "$(git status --porcelain)" ]; then + echo "💿 no docs changed" + exit 0 + fi + git commit -m "chore: generate markdown docs from jsdocs" + git push + echo "💿 pushed docs changes https://github.com/$GITHUB_REPOSITORY/commit/$(git rev-parse HEAD)" From ae97fce46acf0caa46f682fee1aba0a6c9f9a552 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Fri, 11 Jul 2025 20:42:57 +0000 Subject: [PATCH 07/61] chore: generate markdown docs from jsdocs --- docs/api/hooks/useAsyncError.md | 14 ++++++- docs/api/hooks/useAsyncValue.md | 16 ++++++- docs/api/hooks/useHref.md | 24 +++++++---- docs/api/hooks/useInRouterContext.md | 12 ++++++ docs/api/hooks/useLocation.md | 14 ++++++- docs/api/hooks/useMatch.md | 19 ++++++--- docs/api/hooks/useMatches.md | 14 ++++++- docs/api/hooks/useNavigate.md | 63 +++++++++++++++++----------- docs/api/hooks/useNavigation.md | 12 ++++++ docs/api/hooks/useNavigationType.md | 13 +++++- docs/api/hooks/useOutlet.md | 16 +++++-- docs/api/hooks/useOutletContext.md | 17 ++++++-- docs/api/hooks/useParams.md | 24 ++++++++++- docs/api/hooks/useResolvedPath.md | 26 ++++++++---- docs/api/hooks/useRevalidator.md | 21 ++++++++-- docs/api/hooks/useRouteError.md | 18 +++++++- docs/api/hooks/useRouteLoaderData.md | 37 +++++++++------- docs/api/hooks/useRoutes.md | 23 ++++++---- 18 files changed, 297 insertions(+), 86 deletions(-) diff --git a/docs/api/hooks/useAsyncError.md b/docs/api/hooks/useAsyncError.md index 0fc171490b..2cd076244e 100644 --- a/docs/api/hooks/useAsyncError.md +++ b/docs/api/hooks/useAsyncError.md @@ -4,13 +4,24 @@ title: useAsyncError # useAsyncError + + [MODES: framework, data] ## Summary [Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react_router.useAsyncError.html) -Returns the rejection value from the closest [Await](../components/Await). +Returns the rejection value from the closest [``](../components/Await). ```tsx import { Await, useAsyncError } from "react-router"; @@ -34,3 +45,4 @@ function ErrorElement() { ```tsx useAsyncError(): unknown ``` + diff --git a/docs/api/hooks/useAsyncValue.md b/docs/api/hooks/useAsyncValue.md index fc83879809..99704f7728 100644 --- a/docs/api/hooks/useAsyncValue.md +++ b/docs/api/hooks/useAsyncValue.md @@ -4,13 +4,24 @@ title: useAsyncValue # useAsyncValue -[MODES: framework] + + +[MODES: framework, data] ## Summary [Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react_router.useAsyncValue.html) -Returns the resolved promise value from the closest [Await](../components/Await). +Returns the resolved promise value from the closest [``](../components/Await). ```tsx function SomeDescendant() { @@ -29,3 +40,4 @@ function SomeDescendant() { ```tsx useAsyncValue(): unknown ``` + diff --git a/docs/api/hooks/useHref.md b/docs/api/hooks/useHref.md index 02b206b224..4f85f6d593 100644 --- a/docs/api/hooks/useHref.md +++ b/docs/api/hooks/useHref.md @@ -4,6 +4,17 @@ title: useHref # useHref + + [MODES: framework, data, declarative] ## Summary @@ -24,19 +35,18 @@ function SomeComponent() { ## Signature ```tsx -useHref(to, __namedParameters): string +useHref(to: To, { relative }: { + relative?: RelativeRoutingType; +} = {}): string ``` ## Params ### to -[modes: framework, data, declarative] - -_No documentation_ +The path to resolve -### \_\_namedParameters +### options.relative -[modes: framework, data, declarative] +Defaults to "route" so routing is relative to the route tree. Set to "path" to make relative routing operate against path segments. -_No documentation_ diff --git a/docs/api/hooks/useInRouterContext.md b/docs/api/hooks/useInRouterContext.md index 6d0250effb..c46ad50a1a 100644 --- a/docs/api/hooks/useInRouterContext.md +++ b/docs/api/hooks/useInRouterContext.md @@ -4,6 +4,17 @@ title: useInRouterContext # useInRouterContext + + [MODES: framework, data] ## Summary @@ -18,3 +29,4 @@ a component is used within a Router. ```tsx useInRouterContext(): boolean ``` + diff --git a/docs/api/hooks/useLocation.md b/docs/api/hooks/useLocation.md index 5072be97b8..654d737eb6 100644 --- a/docs/api/hooks/useLocation.md +++ b/docs/api/hooks/useLocation.md @@ -4,13 +4,24 @@ title: useLocation # useLocation + + [MODES: framework, data, declarative] ## Summary [Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react_router.useLocation.html) -Returns the current [Location]([../Other/Location](https://api.reactrouter.com/v7/interfaces/react_router.Location.html)). This can be useful if you'd like to perform some side effect whenever it changes. +Returns the current [`Location`](https://api.reactrouter.com/v7/interfaces/react_router.Location.html). This can be useful if you'd like to perform some side effect whenever it changes. ```tsx import * as React from 'react' @@ -35,3 +46,4 @@ function SomeComponent() { ```tsx useLocation(): Location ``` + diff --git a/docs/api/hooks/useMatch.md b/docs/api/hooks/useMatch.md index 5667ef946a..3e7a5b1d9c 100644 --- a/docs/api/hooks/useMatch.md +++ b/docs/api/hooks/useMatch.md @@ -4,6 +4,17 @@ title: useMatch # useMatch + + [MODES: framework, data, declarative] ## Summary @@ -14,19 +25,15 @@ Returns a PathMatch object if the given pattern matches the current URL. This is useful for components that need to know "active" state, e.g. ``. - - ## Signature ```tsx -useMatch(pattern): undefined +useMatch, Path extends string>(pattern: PathPattern | Path): PathMatch | null ``` ## Params ### pattern -[modes: framework, data, declarative] - -_No documentation_ +The pattern to match against the current location diff --git a/docs/api/hooks/useMatches.md b/docs/api/hooks/useMatches.md index af7bb17d00..34b79db4ab 100644 --- a/docs/api/hooks/useMatches.md +++ b/docs/api/hooks/useMatches.md @@ -4,6 +4,17 @@ title: useMatches # useMatches + + [MODES: framework, data] ## Summary @@ -16,5 +27,6 @@ parent/child routes or the route "handle" property ## Signature ```tsx -useMatches(): undefined +useMatches(): UIMatch[] ``` + diff --git a/docs/api/hooks/useNavigate.md b/docs/api/hooks/useNavigate.md index b5be32db54..4c1ac33ed3 100644 --- a/docs/api/hooks/useNavigate.md +++ b/docs/api/hooks/useNavigate.md @@ -4,13 +4,38 @@ title: useNavigate # useNavigate + + [MODES: framework, data, declarative] ## Summary [Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react_router.useNavigate.html) -Returns a function that lets you navigate programmatically in the browser in response to user interactions or effects. +Returns a function that lets you navigate programmatically in the browser in +response to user interactions or effects. + +It's often better to use [`redirect`](../utils/redirect) in [`action`](../../start/framework/route-module#action)/[`loader`](../../start/framework/route-module#loader) functions than this hook. + +The returned function signature is `navigate(to, options?)/navigate(delta)` where: + +* `to` can be a string path, a `To` object, or a number (delta) +* `options` contains options for modifying the navigation + * `flushSync`: Wrap the DOM updates in `ReactDom.flushSync` + * `preventScrollReset`: Do not scroll back to the top of the page after navigation + * `relative`: "route" or "path" to control relative routing logic + * `replace`: Replace the current entry in the history stack + * `state`: Optional history state to include with the new `Location` + * `viewTransition`: Enable `document.startViewTransition` for this navigation ```tsx import { useNavigate } from "react-router"; @@ -18,43 +43,29 @@ import { useNavigate } from "react-router"; function SomeComponent() { let navigate = useNavigate(); return ( - ); } ``` -It's often better to use [redirect](../utils/redirect) in [ActionFunction](https://api.reactrouter.com/v7/interfaces/react_router.ActionFunction.html) and [LoaderFunction](https://api.reactrouter.com/v7/types/react_router.LoaderFunction.html) than this hook. - ## Signature ```tsx -navigate( - to: To, - options?: { - flushSync?: boolean; - preventScrollReset?: boolean; - relative?: RelativeRoutingType; - replace?: boolean; - state?: any; - viewTransition?: boolean; - } -): void | Promise; +useNavigate(): NavigateFunction ``` ## Examples -### Navigate to another path: +### Navigate to another path ```tsx navigate("/some/route"); navigate("/some/route?search=param"); ``` -### Navigate with a `To` object: +### Navigate with a `To` object All properties are optional. @@ -69,7 +80,7 @@ navigate({ If you use `state`, that will be available on the `location` object on the next page. Access it with `useLocation().state` (see [useLocation](./useLocation)). -### Navigate back or forward in the history stack: +### Navigate back or forward in the history stack ```tsx // back @@ -85,7 +96,7 @@ Be cautions with `navigate(number)`. If your application can load up to a route Only use this if you're sure they will have an entry in the history stack to navigate to. -### Replace the current entry in the history stack: +### Replace the current entry in the history stack This will remove the current entry in the history stack, replacing it with a new one, similar to a server side redirect. @@ -95,7 +106,10 @@ navigate("/some/route", { replace: true }); ### Prevent Scroll Reset -[modes: framework, data] +[MODES: framework, data] + +
+
To prevent `` from resetting the scroll position, use the `preventScrollReset` option. @@ -104,3 +118,4 @@ navigate("?some-tab=1", { preventScrollReset: true }); ``` For example, if you have a tab interface connected to search params in the middle of a page and you don't want it to scroll to the top when a tab is clicked. + diff --git a/docs/api/hooks/useNavigation.md b/docs/api/hooks/useNavigation.md index 74c5e1563c..4994730360 100644 --- a/docs/api/hooks/useNavigation.md +++ b/docs/api/hooks/useNavigation.md @@ -4,6 +4,17 @@ title: useNavigation # useNavigation + + [MODES: framework, data] ## Summary @@ -28,3 +39,4 @@ function SomeComponent() { ```tsx useNavigation(): Navigation ``` + diff --git a/docs/api/hooks/useNavigationType.md b/docs/api/hooks/useNavigationType.md index 849d77617a..76a16649c4 100644 --- a/docs/api/hooks/useNavigationType.md +++ b/docs/api/hooks/useNavigationType.md @@ -4,6 +4,17 @@ title: useNavigationType # useNavigationType + + [MODES: framework, data, declarative] ## Summary @@ -13,8 +24,6 @@ title: useNavigationType Returns the current navigation action which describes how the router came to the current location, either by a pop, push, or replace on the history stack. - - ## Signature ```tsx diff --git a/docs/api/hooks/useOutlet.md b/docs/api/hooks/useOutlet.md index a8f6b32a96..3a1d2f7bdb 100644 --- a/docs/api/hooks/useOutlet.md +++ b/docs/api/hooks/useOutlet.md @@ -4,6 +4,17 @@ title: useOutlet # useOutlet + + [MODES: framework, data, declarative] ## Summary @@ -16,13 +27,12 @@ hierarchy. Used internally by `` to render child routes. ## Signature ```tsx -useOutlet(context): undefined +useOutlet(context?: unknown): React.ReactElement | null ``` ## Params ### context -[modes: framework, data, declarative] +The context to pass to the outlet -_No documentation_ diff --git a/docs/api/hooks/useOutletContext.md b/docs/api/hooks/useOutletContext.md index 82bc66dc0c..bf250984bf 100644 --- a/docs/api/hooks/useOutletContext.md +++ b/docs/api/hooks/useOutletContext.md @@ -4,19 +4,28 @@ title: useOutletContext # useOutletContext + + [MODES: framework, data, declarative] ## Summary [Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react_router.useOutletContext.html) -Returns the parent route ``. - - +Returns the parent route [``](../components/Outlet). ## Signature ```tsx -useOutletContext(): Context +useOutletContext(): Context ``` diff --git a/docs/api/hooks/useParams.md b/docs/api/hooks/useParams.md index a20d871701..a89f1a79aa 100644 --- a/docs/api/hooks/useParams.md +++ b/docs/api/hooks/useParams.md @@ -4,6 +4,17 @@ title: useParams # useParams + + [MODES: framework, data, declarative] ## Summary @@ -12,6 +23,8 @@ title: useParams Returns an object of key/value pairs of the dynamic params from the current URL that were matched by the routes. Child routes inherit all params from their parent routes. +Assuming a route pattern like `/posts/:postId` is matched by `/posts/123` then `params.postId` will be `"123"`. + ```tsx import { useParams } from "react-router"; @@ -21,7 +34,15 @@ function SomeComponent() { } ``` -Assuming a route pattern like `/posts/:postId` is matched by `/posts/123` then `params.postId` will be `"123"`. +## Signature + +```tsx +useParams = string>(): Readonly<[ + ParamsOrKey +] extends [ + string +] ? Params : Partial> +``` ## Examples @@ -107,3 +128,4 @@ export default function File() { console.log(catchall); } ``` + diff --git a/docs/api/hooks/useResolvedPath.md b/docs/api/hooks/useResolvedPath.md index 07a252b955..064805fc15 100644 --- a/docs/api/hooks/useResolvedPath.md +++ b/docs/api/hooks/useResolvedPath.md @@ -4,13 +4,24 @@ title: useResolvedPath # useResolvedPath + + [MODES: framework, data, declarative] ## Summary [Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react_router.useResolvedPath.html) -Resolves the pathname of the given `to` value against the current location. Similar to [useHref](../hooks/useHref), but returns a [Path](https://api.reactrouter.com/v7/interfaces/react_router.Path) instead of a string. +Resolves the pathname of the given `to` value against the current location. Similar to [`useHref`](../hooks/useHref), but returns a [`Path`](https://api.reactrouter.com/v7/interfaces/react_router.Path.html) instead of a string. ```tsx import { useResolvedPath } from "react-router"; @@ -27,19 +38,18 @@ function SomeComponent() { ## Signature ```tsx -useResolvedPath(to, __namedParameters): Path +useResolvedPath(to: To, { relative }: { + relative?: RelativeRoutingType; +} = {}): Path ``` ## Params ### to -[modes: framework, data, declarative] - -_No documentation_ +The path to resolve -### \_\_namedParameters +### options.relative -[modes: framework, data, declarative] +Defaults to "route" so routing is relative to the route tree. Set to "path" to make relative routing operate against path segments. -_No documentation_ diff --git a/docs/api/hooks/useRevalidator.md b/docs/api/hooks/useRevalidator.md index 0f02ea7786..c4dd569714 100644 --- a/docs/api/hooks/useRevalidator.md +++ b/docs/api/hooks/useRevalidator.md @@ -4,6 +4,17 @@ title: useRevalidator # useRevalidator + + [MODES: framework, data] ## Summary @@ -28,12 +39,16 @@ function WindowFocusRevalidator() { ); } -``` -Note that page data is already revalidated automatically after actions. If you find yourself using this for normal CRUD operations on your data in response to user interactions, you're probably not taking advantage of the other APIs like [useFetcher](../hooks/useFetcher), [Form](../components/Form), [useSubmit](../hooks/useSubmit) that do this automatically. +Note that page data is already revalidated automatically after actions. If you find yourself using this for normal CRUD operations on your data in response to user interactions, you're probably not taking advantage of the other APIs like [`useFetcher`](../hooks/useFetcher), [`Form`](../components/Form), [`useSubmit`](../hooks/useSubmit) that do this automatically. +``` ## Signature ```tsx -useRevalidator(): undefined +useRevalidator(): { + revalidate: () => Promise; + state: DataRouter["state"]["revalidation"]; +} ``` + diff --git a/docs/api/hooks/useRouteError.md b/docs/api/hooks/useRouteError.md index 20f7e6e6f9..86c5b054c5 100644 --- a/docs/api/hooks/useRouteError.md +++ b/docs/api/hooks/useRouteError.md @@ -4,13 +4,28 @@ title: useRouteError # useRouteError + + [MODES: framework, data] ## Summary [Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react_router.useRouteError.html) -Accesses the error thrown during an [ActionFunction](https://api.reactrouter.com/v7/interfaces/react_router.ActionFunction.html), [LoaderFunction](https://api.reactrouter.com/v7/types/react_router.LoaderFunction.html), or component render to be used in a route module Error Boundary. +Accesses the error thrown during an +[`action`](../../start/framework/route-module#action), +[`loader`](../../start/framework/route-module#loader), +or component render to be used in a route module +[`ErrorBoundary`](../../start/framework/route-module#errorboundary). ```tsx export function ErrorBoundary() { @@ -24,3 +39,4 @@ export function ErrorBoundary() { ```tsx useRouteError(): unknown ``` + diff --git a/docs/api/hooks/useRouteLoaderData.md b/docs/api/hooks/useRouteLoaderData.md index af0e5cc62d..909897f207 100644 --- a/docs/api/hooks/useRouteLoaderData.md +++ b/docs/api/hooks/useRouteLoaderData.md @@ -4,6 +4,17 @@ title: useRouteLoaderData # useRouteLoaderData + + [MODES: framework, data] ## Summary @@ -12,15 +23,8 @@ title: useRouteLoaderData Returns the loader data for a given route by route ID. -```tsx -import { useRouteLoaderData } from "react-router"; - -function SomeComponent() { - const { user } = useRouteLoaderData("root"); -} -``` - -Route IDs are created automatically. They are simply the path of the route file relative to the app folder without the extension. +Route IDs are created automatically. They are simply the path of the route file +relative to the app folder without the extension. | Route Filename | Route ID | | ---------------------------- | ---------------------- | @@ -28,22 +32,27 @@ Route IDs are created automatically. They are simply the path of the route file | `app/routes/teams.tsx` | `"routes/teams"` | | `app/whatever/teams.$id.tsx` | `"whatever/teams.$id"` | -If you created an ID manually, you can use that instead: - ```tsx +import { useRouteLoaderData } from "react-router"; + +function SomeComponent() { + const { user } = useRouteLoaderData("root"); +} + +// You can also specify your own route ID's manually in your routes.ts file: route("/", "containers/app.tsx", { id: "app" }}) +useRouteLoaderData("app"); ``` ## Signature ```tsx -useRouteLoaderData(routeId): undefined +useRouteLoaderData(routeId: string): SerializeFrom | undefined ``` ## Params ### routeId -[modes: framework, data] +The ID of the route to return loader data from -_No documentation_ diff --git a/docs/api/hooks/useRoutes.md b/docs/api/hooks/useRoutes.md index 6ad39ef296..c92859ff11 100644 --- a/docs/api/hooks/useRoutes.md +++ b/docs/api/hooks/useRoutes.md @@ -4,14 +4,24 @@ title: useRoutes # useRoutes + + [MODES: framework, data, declarative] ## Summary [Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react_router.useRoutes.html) -Hook version of [Routes](../components/Routes) that uses objects instead of components. These objects have the same properties as the component props. - +Hook version of [``](../components/Routes) that uses objects instead of components. These objects have the same properties as the component props. The return value of `useRoutes` is either a valid React element you can use to render the route tree, or `null` if nothing matched. ```tsx @@ -41,19 +51,16 @@ function App() { ## Signature ```tsx -useRoutes(routes, locationArg): undefined +useRoutes(routes: RouteObject[], locationArg?: Partial | string): React.ReactElement | null ``` ## Params ### routes -[modes: framework, data, declarative] - -_No documentation_ +An array of route objects that define the route hierarchy ### locationArg -[modes: framework, data, declarative] +An optional location object or pathname string to use instead of the current location -_No documentation_ From 85e7537d3e242916ce7a8f6932aa6d57c397f485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20T=E1=BA=A5n=20Kh=C3=B4i?= <43715386+HelpMe-Pls@users.noreply.github.com> Date: Sun, 13 Jul 2025 01:21:25 +0700 Subject: [PATCH 08/61] docs(how-to/spa): remove duplicated text (#13975) --- contributors.yml | 1 + docs/how-to/spa.md | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/contributors.yml b/contributors.yml index fb7442951a..4f20ad011a 100644 --- a/contributors.yml +++ b/contributors.yml @@ -131,6 +131,7 @@ - haivuw - hampelm - harshmangalam +- HelpMe-Pls - HenriqueLimas - hernanif1 - HK-SHAO diff --git a/docs/how-to/spa.md b/docs/how-to/spa.md index 03a5af2857..28684ac654 100644 --- a/docs/how-to/spa.md +++ b/docs/how-to/spa.md @@ -20,8 +20,6 @@ Typical Single Page apps send a mostly blank `index.html` template with little m - Use React components to generate the initial page users see (root `HydrateFallback`) - Re-enable server rendering later without changing anything about your UI -It's important to note that setting `ssr:false` only disables _runtime server rendering_. React Router will still server render your root route at _build time_ to generate the `index.html` file. This is why your project still needs a dependency on `@react-router/node` and your routes need to be SSR-safe. That means you can't call `window` or other browser-only APIs during the initial render, even when server rendering is disabled. - SPA Mode is a special form of "Pre-Rendering" that allows you to serve all paths in your application from the same HTML file. Please refer to the [Pre-Rendering](./pre-rendering) guide if you want to do more extensive pre-rendering. ## 1. Disable Runtime Server Rendering From c98598f342e4eb165acf7a2c08132f6f4f025ba1 Mon Sep 17 00:00:00 2001 From: Nihal K Date: Sat, 12 Jul 2025 23:52:19 +0530 Subject: [PATCH 09/61] Update useOutletContext.md with detailed information and example (#13982) --- contributors.yml | 1 + docs/api/hooks/useOutletContext.md | 68 ++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/contributors.yml b/contributors.yml index 4f20ad011a..b1b6853c8e 100644 --- a/contributors.yml +++ b/contributors.yml @@ -412,3 +412,4 @@ - zeromask1337 - zheng-chuang - zxTomw +- ioNihal diff --git a/docs/api/hooks/useOutletContext.md b/docs/api/hooks/useOutletContext.md index 82bc66dc0c..04fc13de81 100644 --- a/docs/api/hooks/useOutletContext.md +++ b/docs/api/hooks/useOutletContext.md @@ -20,3 +20,71 @@ Returns the parent route ``. useOutletContext(): Context ``` +
+ Type declaration + +```tsx +declare function useOutletContext< + Context = unknown +>(): Context; +``` + +
+ +Often parent routes manage state or other values you want shared with child routes. You can create your own [context provider](https://react.dev/learn/passing-data-deeply-with-context) if you like, but this is such a common situation that it's built-into ``: + +```tsx lines=[3] +function Parent() { + const [count, setCount] = React.useState(0); + return ; +} +``` + +```tsx lines=[4] +import { useOutletContext } from "react-router-dom"; + +function Child() { + const [count, setCount] = useOutletContext(); + const increment = () => setCount((c) => c + 1); + return ; +} +``` + +If you're using TypeScript, we recommend the parent component provide a custom hook for accessing the context value. This makes it easier for consumers to get nice typings, control consumers, and know who's consuming the context value. Here's a more realistic example: + +```tsx filename=src/routes/dashboard.tsx lines=[13,19] +import * as React from "react"; +import type { User } from "./types"; +import { Outlet, useOutletContext } from "react-router-dom"; + +type ContextType = { user: User | null }; + +export default function Dashboard() { + const [user, setUser] = React.useState(null); + + return ( +
+

Dashboard

+ +
+ ); +} + +export function useUser() { + return useOutletContext(); +} +``` + +```tsx filename=src/routes/dashboard/messages.tsx lines=[1,4] +import { useUser } from "../dashboard"; + +export default function DashboardMessages() { + const { user } = useUser(); + return ( +
+

Messages

+

Hello, {user.name}!

+
+ ); +} +``` From c0e186764e0309415b2de38565995c142f986eb5 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Sat, 12 Jul 2025 18:22:59 +0000 Subject: [PATCH 10/61] chore: format --- contributors.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributors.yml b/contributors.yml index b1b6853c8e..d4b7965d8a 100644 --- a/contributors.yml +++ b/contributors.yml @@ -146,6 +146,7 @@ - igniscyan - imjordanxd - infoxicator +- ioNihal - IsaiStormBlesed - Isammoc - iskanderbroere @@ -412,4 +413,3 @@ - zeromask1337 - zheng-chuang - zxTomw -- ioNihal From 60ffc53ea53c676a5f0b4ff2df76025a0a848029 Mon Sep 17 00:00:00 2001 From: Brooks Lybrand Date: Tue, 15 Jul 2025 09:52:17 -0500 Subject: [PATCH 11/61] docs: correct modes for `createRoutesFromElements` --- docs/api/utils/createRoutesFromElements.md | 6 +++--- docs/start/modes.md | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/api/utils/createRoutesFromElements.md b/docs/api/utils/createRoutesFromElements.md index 20f6b9bd83..c966ff67f1 100644 --- a/docs/api/utils/createRoutesFromElements.md +++ b/docs/api/utils/createRoutesFromElements.md @@ -4,7 +4,7 @@ title: createRoutesFromElements # createRoutesFromElements -[MODES: framework, data, declarative] +[MODES: data] ## Summary @@ -22,12 +22,12 @@ createRoutesFromElements(children, parentPath): undefined ### children -[modes: framework, data, declarative] +[modes: data] _No documentation_ ### parentPath -[modes: framework, data, declarative] +[modes: data] _No documentation_ diff --git a/docs/start/modes.md b/docs/start/modes.md index 055f49ebff..8c724425a3 100644 --- a/docs/start/modes.md +++ b/docs/start/modes.md @@ -182,6 +182,7 @@ This is mostly for the LLMs, but knock yourself out: | createCookieSessionStorage | ✅ | ✅ | | | createMemorySessionStorage | ✅ | ✅ | | | createPath | ✅ | ✅ | ✅ | +| createRoutesFromElements | | ✅ | | | createRoutesStub | ✅ | ✅ | | | createSearchParams | ✅ | ✅ | ✅ | | data | ✅ | ✅ | | From 422186644b4af7e0fae2934871ec9b0cddb5c757 Mon Sep 17 00:00:00 2001 From: "Huy \"Hugh\" Nguyen" Date: Tue, 15 Jul 2025 13:48:32 -0400 Subject: [PATCH 12/61] docs: fix grammar (#14000) --- CHANGELOG.md | 4 ++-- docs/start/modes.md | 6 +++--- docs/upgrading/component-routes.md | 2 +- docs/upgrading/remix.md | 2 +- docs/upgrading/router-provider.md | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfbb78e0a2..c742f9675b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,7 +100,7 @@ We manage release notes in this file instead of the paginated Github Releases Pa - [Exposed Router Promises](#exposed-router-promises) - [Other Notable Changes](#other-notable-changes) - [`routes.ts`](#routests) - - [Typesafety improvements](#typesafety-improvements) + - [Type-safety improvements](#type-safety-improvements) - [Prerendering](#prerendering) - [Major Changes (`react-router`)](#major-changes-react-router) - [Major Changes (`@react-router/*`)](#major-changes-react-router-1) @@ -1860,7 +1860,7 @@ Also note that, if you were using Remix's `routes` option to define config-based +]; ``` -#### Typesafety improvements +#### Type-safety improvements React Router now generates types for each of your route modules and passes typed props to route module component exports ([#11961](https://github.com/remix-run/react-router/pull/11961), [#12019](https://github.com/remix-run/react-router/pull/12019)). You can access those types by importing them from `./+types/`. diff --git a/docs/start/modes.md b/docs/start/modes.md index 8c724425a3..47745aa7d5 100644 --- a/docs/start/modes.md +++ b/docs/start/modes.md @@ -56,8 +56,8 @@ ReactDOM.createRoot(root).render( Framework Mode wraps Data Mode with a Vite plugin to add the full React Router experience with: -- typesafe `href` -- typesafe Route Module API +- type-safe `href` +- type-safe Route Module API - intelligent code splitting - SPA, SSR, and static rendering strategies - and more @@ -71,7 +71,7 @@ export default [ ]; ``` -You'll then have access to the Route Module API with typesafe params, loaderData, code splitting, SPA/SSR/SSG strategies, and more. +You'll then have access to the Route Module API with type-safe params, loaderData, code splitting, SPA/SSR/SSG strategies, and more. ```ts filename=product.tsx import { Route } from "+./types/product.tsx"; diff --git a/docs/upgrading/component-routes.md b/docs/upgrading/component-routes.md index 6b4f7824b1..0be48e1ea6 100644 --- a/docs/upgrading/component-routes.md +++ b/docs/upgrading/component-routes.md @@ -16,7 +16,7 @@ The React Router Vite plugin adds framework features to React Router. This guide The Vite plugin adds: - Route loaders, actions, and automatic data revalidation -- Typesafe Routes Modules +- Type-safe Routes Modules - Automatic route code-splitting - Automatic scroll restoration across navigations - Optional Static pre-rendering diff --git a/docs/upgrading/remix.md b/docs/upgrading/remix.md index 432b3c1011..c7f3274f2c 100644 --- a/docs/upgrading/remix.md +++ b/docs/upgrading/remix.md @@ -394,7 +394,7 @@ Congratulations! You are now on React Router v7. Go ahead and run your applicati [v2-future-flags]: https://remix.run/docs/start/future-flags [routing]: ../start/framework/routing [fs-routing]: ../how-to/file-route-conventions -[v7-changelog-types]: https://github.com/remix-run/react-router/blob/release-next/CHANGELOG.md#typesafety-improvements +[v7-changelog-types]: https://github.com/remix-run/react-router/blob/release-next/CHANGELOG.md#type-safety-improvements [server-loaders]: ../start/framework/data-loading#server-data-loading [server-actions]: ../start/framework/actions#server-actions [ts-module-augmentation]: https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation diff --git a/docs/upgrading/router-provider.md b/docs/upgrading/router-provider.md index 423b71aa02..6b14d05294 100644 --- a/docs/upgrading/router-provider.md +++ b/docs/upgrading/router-provider.md @@ -14,7 +14,7 @@ The React Router Vite plugin adds framework features to React Router. This guide The Vite plugin adds: - Route loaders, actions, and automatic data revalidation -- Typesafe Routes Modules +- Type-safe Routes Modules - Automatic route code-splitting - Automatic scroll restoration across navigations - Optional Static pre-rendering From 7fcb5fee7da5b0353a2a70099fa75a3784ec3528 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 15 Jul 2025 16:57:10 -0400 Subject: [PATCH 13/61] Auto-generate docs for DOM-related APIs (#14002) --- .eslintrc | 6 +- .github/workflows/docs.yml | 15 +- .../unstable_HistoryRouter.md | 36 - packages/react-router/index.ts | 6 +- packages/react-router/lib/dom/lib.tsx | 1555 ++++++++++------- packages/react-router/lib/dom/server.tsx | 4 +- .../react-router/lib/dom/ssr/components.tsx | 8 +- packages/react-router/lib/router/router.ts | 8 +- scripts/docs.ts | 387 ++-- 9 files changed, 1242 insertions(+), 783 deletions(-) delete mode 100644 docs/api/declarative-routers/unstable_HistoryRouter.md diff --git a/.eslintrc b/.eslintrc index 62ab116a15..b071ec4c5c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -21,7 +21,10 @@ }, { // Only apply JSDoc lint rules to files we auto-generate docs for - "files": ["packages/react-router/lib/hooks.tsx"], + "files": [ + "packages/react-router/lib/hooks.tsx", + "packages/react-router/lib/dom/lib.tsx" + ], "plugins": ["jsdoc"], "rules": { "jsdoc/check-access": "error", @@ -58,6 +61,7 @@ }, { "tags": [ + "name", "public", "private", "category", diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 50ee6cec89..bd98ccb8fa 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -11,10 +11,6 @@ on: branch: description: "Branch to generate docs for (usually dev)" required: true - api: - description: "API Names to generate docs for" - required: false - default: "" concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -50,13 +46,10 @@ jobs: - name: 📚 Generate Typedoc Docs run: pnpm run docs - - name: 📚 Generate Markdown Docs (for all APIs) - if: github.event.inputs.api == '' - run: node --experimental-strip-types scripts/docs.ts --path packages/react-router/lib/hooks.tsx --write - - - name: 📚 Generate Markdown Docs (for specific APIs) - if: github.event.inputs.api != '' - run: node --experimental-strip-types scripts/docs.ts --path packages/react-router/lib/hooks.tsx --write --api ${{ github.event.inputs.api }} + - name: 📚 Generate Markdown Docs + run: | + node --experimental-strip-types scripts/docs.ts --path packages/react-router/lib/hooks.tsx --write + node --experimental-strip-types scripts/docs.ts --path packages/react-router/lib/dom/lib.tsx --write - name: 💪 Commit run: | diff --git a/docs/api/declarative-routers/unstable_HistoryRouter.md b/docs/api/declarative-routers/unstable_HistoryRouter.md deleted file mode 100644 index 02e5db776e..0000000000 --- a/docs/api/declarative-routers/unstable_HistoryRouter.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: HistoryRouter ---- - -# HistoryRouter - -[MODES: declarative] - -## Summary - -[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react_router.unstable_HistoryRouter.html) - -A `` that accepts a pre-instantiated history object. It's important -to note that using your own history object is highly discouraged and may add -two versions of the history library to your bundles unless you use the same -version of the history library that React Router uses internally. - -## Props - -### basename - -[modes: declarative] - -_No documentation_ - -### children - -[modes: declarative] - -_No documentation_ - -### history - -[modes: declarative] - -_No documentation_ diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts index 1d7b565cd9..323feb0cfe 100644 --- a/packages/react-router/index.ts +++ b/packages/react-router/index.ts @@ -204,7 +204,11 @@ export { Scripts, PrefetchPageLinks, } from "./lib/dom/ssr/components"; -export type { ScriptsProps } from "./lib/dom/ssr/components"; +export type { + ScriptsProps, + PrefetchBehavior, + DiscoverBehavior, +} from "./lib/dom/ssr/components"; export type { EntryContext } from "./lib/dom/ssr/entry"; export type { ClientActionFunction, diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx index 7147e3a8d1..87724ecc41 100644 --- a/packages/react-router/lib/dom/lib.tsx +++ b/packages/react-router/lib/dom/lib.tsx @@ -130,7 +130,7 @@ try { //////////////////////////////////////////////////////////////////////////////// /** - * @category Routers + * @category Data Routers */ export interface DOMRouterOpts { /** @@ -169,16 +169,22 @@ export interface DOMRouterOpts { * Create a new data router that manages the application path via `history.pushState` * and `history.replaceState`. * + * @public * @category Data Routers + * @mode data + * @param routes Application routes + * @param opts Options + * @param {DOMRouterOpts.basename} opts.basename n/a + * @param {DOMRouterOpts.dataStrategy} opts.dataStrategy n/a + * @param {DOMRouterOpts.future} opts.future n/a + * @param {DOMRouterOpts.hydrationData} opts.hydrationData n/a + * @param {DOMRouterOpts.patchRoutesOnNavigation} opts.patchRoutesOnNavigation n/a + * @param {DOMRouterOpts.unstable_getContext} opts.unstable_getContext n/a + * @param {DOMRouterOpts.window} opts.window n/a + * @returns An initialized data router to pass to {@link RouterProvider | ``} */ export function createBrowserRouter( - /** - * Application routes - */ routes: RouteObject[], - /** - * Router options - */ opts?: DOMRouterOpts ): DataRouter { return createRouter({ @@ -197,9 +203,21 @@ export function createBrowserRouter( } /** - * Create a new data router that manages the application path via the URL hash + * Create a new data router that manages the application path via the URL hash. * + * @public * @category Data Routers + * @mode data + * @param routes Application routes + * @param opts Options + * @param {DOMRouterOpts.basename} opts.basename n/a + * @param {DOMRouterOpts.unstable_getContext} opts.unstable_getContext n/a + * @param {DOMRouterOpts.future} opts.future n/a + * @param {DOMRouterOpts.hydrationData} opts.hydrationData n/a + * @param {DOMRouterOpts.dataStrategy} opts.dataStrategy n/a + * @param {DOMRouterOpts.patchRoutesOnNavigation} opts.patchRoutesOnNavigation n/a + * @param {DOMRouterOpts.window} opts.window n/a + * @returns An initialized data router to pass to {@link RouterProvider | ``} */ export function createHashRouter( routes: RouteObject[], @@ -295,9 +313,16 @@ export interface BrowserRouterProps { } /** - * A `` for use in web browsers. Provides the cleanest URLs. + * A declarative `` using the browser history API for client side routing. * - * @category Component Routers + * @public + * @category Declarative Routers + * @mode declarative + * @param props Props + * @param props.basename Application basename + * @param props.children {@link Route | ``} components describing your route configuration + * @param props.window Window object override - defaults to the global `window` instance + * @returns A declarative router using the browser history API for client side routing. */ export function BrowserRouter({ basename, @@ -344,10 +369,17 @@ export interface HashRouterProps { } /** - * A `` for use in web browsers. Stores the location in the hash - * portion of the URL so it is not sent to the server. + * A declarative `` that stores the location in the hash portion of the + * URL so it is not sent to the server. * - * @category Component Routers + * @public + * @category Declarative Routers + * @mode declarative + * @param props Props + * @param props.basename Application basename + * @param props.children {@link Route | ``} components describing your route configuration + * @param props.window Window object override - defaults to the global `window` instance + * @returns A declarative router using the URL hash for client side routing. */ export function HashRouter({ basename, children, window }: HashRouterProps) { let historyRef = React.useRef(); @@ -390,13 +422,20 @@ export interface HistoryRouterProps { } /** - * A `` that accepts a pre-instantiated history object. It's important - * to note that using your own history object is highly discouraged and may add - * two versions of the history library to your bundles unless you use the same - * version of the history library that React Router uses internally. + * A declarative `` that accepts a pre-instantiated history object. + * It's important to note that using your own history object is highly discouraged + * and may add two versions of the history library to your bundles unless you use + * the same version of the history library that React Router uses internally. * * @name unstable_HistoryRouter - * @category Component Routers + * @public + * @category Declarative Routers + * @mode declarative + * @param props Props + * @param props.basename Application basename + * @param props.children {@link Route | ``} components describing your route configuration + * @param props.history History implementation for use by the router + * @returns A declarative router using the URL hash for client side routing. */ export function HistoryRouter({ basename, @@ -434,148 +473,165 @@ HistoryRouter.displayName = "unstable_HistoryRouter"; export interface LinkProps extends Omit, "href"> { /** - Defines the link discovery behavior - - ```tsx - // default ("render") - - - ``` - - - **render** - default, discover the route when the link renders - - **none** - don't eagerly discover, only discover if the link is clicked - */ + * Defines the link discovery behavior + * + * ```tsx + * // default ("render") + * + * + * ``` + * + * - **render** - default, discover the route when the link renders + * - **none** - don't eagerly discover, only discover if the link is clicked + */ discover?: DiscoverBehavior; /** - Defines the data and module prefetching behavior for the link. - - ```tsx - // default - - - - - ``` - - - **none** - default, no prefetching - - **intent** - prefetches when the user hovers or focuses the link - - **render** - prefetches when the link renders - - **viewport** - prefetches when the link is in the viewport, very useful for mobile - - Prefetching is done with HTML `` tags. They are inserted after the link. - - ```tsx - - - // might conditionally render - ``` - - Because of this, if you are using `nav :last-child` you will need to use `nav :last-of-type` so the styles don't conditionally fall off your last link (and any other similar selectors). + * Defines the data and module prefetching behavior for the link. + * + * ```tsx + * // default + * + * + * + * + * ``` + * + * - **none** - default, no prefetching + * - **intent** - prefetches when the user hovers or focuses the link + * - **render** - prefetches when the link renders + * - **viewport** - prefetches when the link is in the viewport, very useful for mobile + * + * Prefetching is done with HTML `` tags. They are inserted + * after the link. + * + * ```tsx + * + * + * // might conditionally render + * ``` + * + * Because of this, if you are using `nav :last-child` you will need to use + * `nav :last-of-type` so the styles don't conditionally fall off your last link + * (and any other similar selectors). */ prefetch?: PrefetchBehavior; /** - Will use document navigation instead of client side routing when the link is clicked: the browser will handle the transition normally (as if it were an ``). - - ```tsx - - ``` + * Will use document navigation instead of client side routing when the link is + * clicked: the browser will handle the transition normally (as if it were an ``). + * + * ```tsx + * + * ``` */ reloadDocument?: boolean; /** - Replaces the current entry in the history stack instead of pushing a new one onto it. - - ```tsx - - ``` - - ``` - # with a history stack like this - A -> B - - # normal link click pushes a new entry - A -> B -> C - - # but with `replace`, B is replaced by C - A -> C - ``` + * Replaces the current entry in the history stack instead of pushing a new one + * onto it. + * + * ```tsx + * + * ``` + * + * ``` + * # with a history stack like this + * A -> B + * + * # normal link click pushes a new entry + * A -> B -> C + * + * # but with `replace`, B is replaced by C + * A -> C + * ``` */ replace?: boolean; /** - Adds persistent client side routing state to the next location. - - ```tsx - - ``` - - The location state is accessed from the `location`. - - ```tsx - function SomeComp() { - const location = useLocation() - location.state; // { some: "value" } - } - ``` - - This state is inaccessible on the server as it is implemented on top of [`history.state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state) + * Adds persistent client side routing state to the next location. + * + * ```tsx + * + * ``` + * + * The location state is accessed from the `location`. + * + * ```tsx + * function SomeComp() { + * const location = useLocation(); + * location.state; // { some: "value" } + * } + * ``` + * + * This state is inaccessible on the server as it is implemented on top of + * [`history.state`](https://developer.mozilla.org/en-US/docs/Web/API/History/state) */ state?: any; /** - Prevents the scroll position from being reset to the top of the window when the link is clicked and the app is using {@link ScrollRestoration}. This only prevents new locations reseting scroll to the top, scroll position will be restored for back/forward button navigation. - - ```tsx - - ``` + * Prevents the scroll position from being reset to the top of the window when + * the link is clicked and the app is using {@link ScrollRestoration}. This only + * prevents new locations reseting scroll to the top, scroll position will be + * restored for back/forward button navigation. + * + * ```tsx + * + * ``` */ preventScrollReset?: boolean; /** - Defines the relative path behavior for the link. - - ```tsx - // default: "route" - - - ``` - - Consider a route hierarchy where a parent route pattern is "blog" and a child route pattern is "blog/:slug/edit". - - - **route** - default, resolves the link relative to the route pattern. In the example above a relative link of `".."` will remove both `:slug/edit` segments back to "/blog". - - **path** - relative to the path so `..` will only remove one URL segment up to "/blog/:slug" + * Defines the relative path behavior for the link. + * + * ```tsx + * // default: "route" + * + * + * ``` + * + * Consider a route hierarchy where a parent route pattern is "blog" and a child + * route pattern is "blog/:slug/edit". + * + * - **route** - default, resolves the link relative to the route pattern. In the + * example above a relative link of `".."` will remove both `:slug/edit` segments + * back to "/blog". + * - **path** - relative to the path so `..` will only remove one URL segment up + * to "/blog/:slug" + * + * Note that index routes and layout routes do not have paths so they are not + * included in the relative path calculation. */ relative?: RelativeRoutingType; /** - Can be a string or a partial {@link Path}: - - ```tsx - - - - ``` + * Can be a string or a partial {@link Path}: + * + * ```tsx + * + * + * + * ``` */ to: To; /** - Enables a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) for this navigation. - - ```jsx - - Click me - - ``` - - To apply specific styles for the transition, see {@link useViewTransitionState} + * Enables a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) + * for this navigation. + * + * ```jsx + * + * Click me + * + * ``` + * + * To apply specific styles for the transition, see {@link useViewTransitionState} */ viewTransition?: boolean; } @@ -583,23 +639,32 @@ export interface LinkProps const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i; /** - A progressively enhanced `` wrapper to enable navigation with client-side routing. - - ```tsx - import { Link } from "react-router"; - - Dashboard; - - - ``` - - @category Components + * A progressively enhanced `` wrapper to enable navigation with client-side routing. + * + * @example + * import { Link } from "react-router"; + * + * Dashboard; + * + * ; + * + * @public + * @category Components + * @param {LinkProps.discover} props.discover [modes: framework] + * @param {LinkProps.prefetch} props.prefetch [modes: framework] + * @param {LinkProps.preventScrollReset} props.preventScrollReset [modes: framework, data] + * @param {LinkProps.relative} props.relative + * @param {LinkProps.reloadDocument} props.reloadDocument + * @param {LinkProps.replace} props.replace + * @param {LinkProps.state} props.state + * @param {LinkProps.to} props.to + * @param {LinkProps.viewTransition} props.viewTransition [modes: framework, data] */ export const Link = React.forwardRef( function LinkWithRef( @@ -708,38 +773,39 @@ export const Link = React.forwardRef( Link.displayName = "Link"; /** - The object passed to {@link NavLink} `children`, `className`, and `style` prop callbacks to render and style the link based on its state. - - ``` - // className - - isPending ? "pending" : isActive ? "active" : "" - } - > - Messages - - - // style - { - return { - fontWeight: isActive ? "bold" : "", - color: isPending ? "red" : "black", - } - )} - /> - - // children - - {({ isActive, isPending }) => ( - Tasks - )} - - ``` - + * The object passed to {@link NavLink} `children`, `className`, and `style` prop + * callbacks to render and style the link based on its state. + * + * ``` + * // className + * + * isPending ? "pending" : isActive ? "active" : "" + * } + * > + * Messages + * + * + * // style + * { + * return { + * fontWeight: isActive ? "bold" : "", + * color: isPending ? "red" : "black", + * } + * )} + * /> + * + * // children + * + * {({ isActive, isPending }) => ( + * Tasks + * )} + * + * ``` + * */ export type NavLinkRenderProps = { /** @@ -748,12 +814,14 @@ export type NavLinkRenderProps = { isActive: boolean; /** - * Indicates if the pending location matches the link's URL. + * Indicates if the pending location matches the link's URL. Only available in + * Framework/Data modes. */ isPending: boolean; /** - * Indicates if a view transition to the link's URL is in progress. See {@link useViewTransitionState} + * Indicates if a view transition to the link's URL is in progress. + * See {@link useViewTransitionState} */ isTransitioning: boolean; }; @@ -764,85 +832,135 @@ export type NavLinkRenderProps = { export interface NavLinkProps extends Omit { /** - Can be regular React children or a function that receives an object with the active and pending states of the link. - - ```tsx - - {({ isActive }) => ( - Tasks - )} - - ``` + * Can be regular React children or a function that receives an object with the + * `active` and `pending` states of the link. + * + * ```tsx + * + * {({ isActive }) => ( + * Tasks + * )} + * + * ``` */ children?: React.ReactNode | ((props: NavLinkRenderProps) => React.ReactNode); /** - Changes the matching logic to make it case-sensitive: - - | Link | URL | isActive | - | -------------------------------------------- | ------------- | -------- | - | `` | `/sponge-bob` | true | - | `` | `/sponge-bob` | false | + * Changes the matching logic to make it case-sensitive: + * + * | Link | URL | isActive | + * | -------------------------------------------- | ------------- | -------- | + * | `` | `/sponge-bob` | true | + * | `` | `/sponge-bob` | false | */ caseSensitive?: boolean; /** - Classes are automatically applied to NavLink that correspond to {@link NavLinkRenderProps}. - - ```css - a.active { color: red; } - a.pending { color: blue; } - a.transitioning { - view-transition-name: my-transition; - } - ``` + * Classes are automatically applied to NavLink that correspond to the state. + * + * ```css + * a.active { + * color: red; + * } + * a.pending { + * color: blue; + * } + * a.transitioning { + * view-transition-name: my-transition; + * } + * ``` + * + * Or you can specify a function that receives {@link NavLinkRenderProps} and + * returns the `className`: + * + * ```tsx + * ( + * isActive ? "my-active-class" : + * isPending ? "my-pending-class" : + * "" + * )} /> + * ``` */ className?: string | ((props: NavLinkRenderProps) => string | undefined); /** - Changes the matching logic for the `active` and `pending` states to only match to the "end" of the {@link NavLinkProps.to}. If the URL is longer, it will no longer be considered active. - - | Link | URL | isActive | - | ----------------------------- | ------------ | -------- | - | `` | `/tasks` | true | - | `` | `/tasks/123` | true | - | `` | `/tasks` | true | - | `` | `/tasks/123` | false | - - `` is an exceptional case because _every_ URL matches `/`. To avoid this matching every single route by default, it effectively ignores the `end` prop and only matches when you're at the root route. + * Changes the matching logic for the `active` and `pending` states to only match + * to the "end" of the {@link NavLinkProps.to}. If the URL is longer, it will no + * longer be considered active. + * + * | Link | URL | isActive | + * | ----------------------------- | ------------ | -------- | + * | `` | `/tasks` | true | + * | `` | `/tasks/123` | true | + * | `` | `/tasks` | true | + * | `` | `/tasks/123` | false | + * + * `` is an exceptional case because _every_ URL matches `/`. + * To avoid this matching every single route by default, it effectively ignores + * the `end` prop and only matches when you're at the root route. */ end?: boolean; + /** + * Styles can also be applied dynamically via a function that receives + * `NavLinkRenderProps` and returns the styles: + * + * ```tsx + * + * ({ + * color: + * isActive ? "red" : + * isPending ? "blue" : "black" + * })} /> + * ``` + */ style?: | React.CSSProperties | ((props: NavLinkRenderProps) => React.CSSProperties | undefined); } /** - Wraps {@link Link | ``} with additional props for styling active and pending states. - - - Automatically applies classes to the link based on its active and pending states, see {@link NavLinkProps.className}. - - Automatically applies `aria-current="page"` to the link when the link is active. See [`aria-current`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current) on MDN. - - ```tsx - import { NavLink } from "react-router" - - ``` - - States are available through the className, style, and children render props. See {@link NavLinkRenderProps}. - - ```tsx - - isPending ? "pending" : isActive ? "active" : "" - } - > - Messages - - ``` - - @category Components + * Wraps {@link Link | ``} with additional props for styling active and + * pending states. + * + * - Automatically applies classes to the link based on its `active` and `pending` + * states, see {@link NavLinkProps.className} + * - Note that `pending` is only available with Framework and Data modes. + * - Automatically applies `aria-current="page"` to the link when the link is active. + * See [`aria-current`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current) + * on MDN. + * - States are additionally available through the className, style, and children + * render props. See {@link NavLinkRenderProps}. + * + * @example + * Messages + * + * // Using render props + * + * isPending ? "pending" : isActive ? "active" : "" + * } + * > + * Messages + * + * + * @public + * @category Components + * @param {NavLinkProps.caseSensitive} props.caseSensitive + * @param {NavLinkProps.children} props.children + * @param {NavLinkProps.className} props.className + * @param {NavLinkProps.discover} props.discover [modes: framework] + * @param {NavLinkProps.end} props.end + * @param {NavLinkProps.prefetch} props.prefetch [modes: framework] + * @param {NavLinkProps.preventScrollReset} props.preventScrollReset [modes: framework, data] + * @param {NavLinkProps.relative} props.relative + * @param {NavLinkProps.reloadDocument} props.reloadDocument + * @param {NavLinkProps.replace} props.replace + * @param {NavLinkProps.state} props.state + * @param {NavLinkProps.style} props.style + * @param {NavLinkProps.to} props.to + * @param {NavLinkProps.viewTransition} props.viewTransition [modes: framework, data] */ export const NavLink = React.forwardRef( function NavLinkWithRef( @@ -976,6 +1094,12 @@ interface SharedFormProps extends React.FormHTMLAttributes { /** * The encoding type to use for the form submission. + * + * ```tsx + *
// Default + * + * + * ``` */ encType?: | "application/x-www-form-urlencoded" @@ -983,14 +1107,16 @@ interface SharedFormProps extends React.FormHTMLAttributes { | "text/plain"; /** - * The URL to submit the form data to. If `undefined`, this defaults to the closest route in context. + * The URL to submit the form data to. If `undefined`, this defaults to the + * closest route in context. */ action?: string; /** * Determines whether the form action is relative to the route hierarchy or * the pathname. Use this if you want to opt out of navigating the route - * hierarchy and want to instead route based on /-delimited URL segments + * hierarchy and want to instead route based on slash-delimited URL segments. + * See {@link RelativeRoutingType}. */ relative?: RelativeRoutingType; @@ -1018,6 +1144,18 @@ export interface FetcherFormProps extends SharedFormProps {} * @category Types */ export interface FormProps extends SharedFormProps { + /** + * Defines the link discovery behavior. See {@link DiscoverBehavior}. + * + * ```tsx + * // default ("render") + * + * + * ``` + * + * - **render** - default, discover the route when the link renders + * - **none** - don't eagerly discover, only discover if the link is clicked + */ discover?: DiscoverBehavior; /** @@ -1028,10 +1166,9 @@ export interface FormProps extends SharedFormProps { fetcherKey?: string; /** - * Skips the navigation and uses a {@link useFetcher | fetcher} internally - * when `false`. This is essentially a shorthand for `useFetcher()` + - * `` where you don't care about the resulting data in this - * component. + * When `false`, skips the navigation and submits via a fetcher internally. + * This is essentially a shorthand for {@link useFetcher} + `` where + * you don't care about the resulting data in this component. */ navigate?: boolean; @@ -1071,28 +1208,43 @@ type HTMLSubmitEvent = React.BaseSyntheticEvent< type HTMLFormSubmitter = HTMLButtonElement | HTMLInputElement; /** - -A progressively enhanced HTML [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) that submits data to actions via `fetch`, activating pending states in `useNavigation` which enables advanced user interfaces beyond a basic HTML form. After a form's action completes, all data on the page is automatically revalidated to keep the UI in sync with the data. - -Because it uses the HTML form API, server rendered pages are interactive at a basic level before JavaScript loads. Instead of React Router managing the submission, the browser manages the submission as well as the pending states (like the spinning favicon). After JavaScript loads, React Router takes over enabling web application user experiences. - -Form is most useful for submissions that should also change the URL or otherwise add an entry to the browser history stack. For forms that shouldn't manipulate the browser history stack, use [``][fetcher_form]. - -```tsx -import { Form } from "react-router"; - -function NewEvent() { - return ( - - - - - ) -} -``` - -@category Components -*/ + * A progressively enhanced HTML [`
`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) that submits data to actions via `fetch`, activating pending states in `useNavigation` which enables advanced user interfaces beyond a basic HTML form. After a form's action completes, all data on the page is automatically revalidated to keep the UI in sync with the data. + * + * Because it uses the HTML form API, server rendered pages are interactive at a basic level before JavaScript loads. Instead of React Router managing the submission, the browser manages the submission as well as the pending states (like the spinning favicon). After JavaScript loads, React Router takes over enabling web application user experiences. + * + * Form is most useful for submissions that should also change the URL or otherwise add an entry to the browser history stack. For forms that shouldn't manipulate the browser history stack, use [``][fetcher_form]. + * + * ```tsx + * import { Form } from "react-router"; + * + * function NewEvent() { + * return ( + * + * + * + * + * ); + * } + * ``` + * @public + * @category Components + * @mode framework + * @mode data + * @param {FormProps.action} action n/a + * @param {FormProps.discover} discover n/a + * @param {FormProps.encType} encType n/a + * @param {FormProps.fetcherKey} fetcherKey n/a + * @param {FormProps.method} method n/a + * @param {FormProps.navigate} navigate n/a + * @param {FormProps.onSubmit} onSubmit n/a + * @param {FormProps.preventScrollReset} preventScrollReset n/a + * @param {FormProps.relative} relative n/a + * @param {FormProps.reloadDocument} reloadDocument n/a + * @param {FormProps.replace} replace n/a + * @param {FormProps.state} state n/a + * @param {FormProps.viewTransition} viewTransition n/a + * @returns A progressively enhanced `
` component + */ export const Form = React.forwardRef( ( { @@ -1161,47 +1313,65 @@ Form.displayName = "Form"; export type ScrollRestorationProps = ScriptsProps & { /** - Defines the key used to restore scroll positions. - - ```tsx - { - // default behavior - return location.key - }} - /> - ``` + * A function that returns a key to use for scroll restoration. This is useful + * for custom scroll restoration logic, such as using only the pathname so + * that subsequent navigations to prior paths will restore the scroll. Defaults + * to `location.key`. See {@link GetScrollRestorationKeyFunction}. + * + * ```tsx + * { + * // Restore based on unique location key (default behavior) + * return location.key + * + * // Restore based on pathname + * return location.pathname + * }} + * /> + * ``` */ getKey?: GetScrollRestorationKeyFunction; + /** + * The key to use for storing scroll positions in `sessionStorage`. Defaults + * to `"react-router-scroll-positions"`. + */ storageKey?: string; }; /** - Emulates the browser's scroll restoration on location changes. Apps should only render one of these, right before the {@link Scripts} component. - - ```tsx - import { ScrollRestoration } from "react-router"; - - export default function Root() { - return ( - - - - - - - ); - } - ``` - - This component renders an inline `` + ``, ); }); @@ -452,7 +452,7 @@ describe("A ", () => { let context = (await query( new Request("http://localhost/the/path", { signal: new AbortController().signal, - }) + }), )) as StaticHandlerContext; let html = ReactDOMServer.renderToStaticMarkup( @@ -461,7 +461,7 @@ describe("A ", () => { router={createStaticRouter(dataRoutes, context)} context={context} /> - + , ); expect(html).toMatch("

👋

"); @@ -473,10 +473,10 @@ describe("A ", () => { }, actionData: null, errors: null, - }) + }), ); expect(html).toMatch( - `` + ``, ); }); @@ -495,7 +495,7 @@ describe("A ", () => { let context = (await query( new Request("http://localhost/", { signal: new AbortController().signal, - }) + }), )) as StaticHandlerContext; let html = ReactDOMServer.renderToStaticMarkup( @@ -504,10 +504,10 @@ describe("A ", () => { router={createStaticRouter(routes, context)} context={context} /> - + , ); expect(html).toMatchInlineSnapshot( - `"

👋

"` + `"

👋

"`, ); }); @@ -518,7 +518,7 @@ describe("A ", () => { let context = (await query( new Request("http://localhost/path/with%20space", { signal: new AbortController().signal, - }) + }), )) as StaticHandlerContext; let html = ReactDOMServer.renderToStaticMarkup( @@ -527,10 +527,10 @@ describe("A ", () => { router={createStaticRouter(routes, context)} context={context} /> - + , ); expect(html).toContain( - '
👋' + '👋', ); }); @@ -543,7 +543,7 @@ describe("A ", () => { let context = (await query( new Request("http://localhost/", { signal: new AbortController().signal, - }) + }), )) as StaticHandlerContext; let html = ReactDOMServer.renderToStaticMarkup( @@ -552,10 +552,10 @@ describe("A ", () => { router={createStaticRouter(routes, context)} context={context} /> - + , ); expect(html).toContain( - '👋' + '👋', ); }); @@ -566,7 +566,7 @@ describe("A ", () => { let context = (await query( new Request("http://localhost/path/with%20space", { signal: new AbortController().signal, - }) + }), )) as StaticHandlerContext; let html = ReactDOMServer.renderToStaticMarkup( @@ -575,10 +575,10 @@ describe("A ", () => { router={createStaticRouter(routes, context)} context={context} /> - + , ); expect(html).toContain( - '👋' + '
👋
', ); }); @@ -591,7 +591,7 @@ describe("A ", () => { let context = (await query( new Request("http://localhost/path/with%20space", { signal: new AbortController().signal, - }) + }), )) as StaticHandlerContext; let html = ReactDOMServer.renderToStaticMarkup( @@ -600,10 +600,10 @@ describe("A ", () => { router={createStaticRouter(routes, context)} context={context} /> - + , ); expect(html).toContain( - '
👋
' + '
👋
', ); }); @@ -616,7 +616,7 @@ describe("A ", () => { let context = (await query( new Request("http://localhost/", { signal: new AbortController().signal, - }) + }), )) as StaticHandlerContext; let html = ReactDOMServer.renderToStaticMarkup( @@ -625,10 +625,10 @@ describe("A ", () => { router={createStaticRouter(routes, context)} context={context} /> - + , ); expect(html).toContain( - '
👋
' + '
👋
', ); }); @@ -639,7 +639,7 @@ describe("A ", () => { loader: () => { throw Response.json( { not: "found" }, - { status: 404, statusText: "Not Found" } + { status: 404, statusText: "Not Found" }, ); }, }, @@ -649,7 +649,7 @@ describe("A ", () => { let context = (await query( new Request("http://localhost/", { signal: new AbortController().signal, - }) + }), )) as StaticHandlerContext; let html = ReactDOMServer.renderToStaticMarkup( @@ -658,7 +658,7 @@ describe("A ", () => { router={createStaticRouter(routes, context)} context={context} /> - + , ); let expectedJsonString = JSON.stringify( @@ -674,10 +674,10 @@ describe("A ", () => { __type: "RouteErrorResponse", }, }, - }) + }), ); expect(html).toMatch( - `` + ``, ); }); @@ -689,7 +689,7 @@ describe("A ", () => { loader: () => { throw Response.json( { not: "found" }, - { status: 404, statusText: "Not Found" } + { status: 404, statusText: "Not Found" }, ); }, }), @@ -700,7 +700,7 @@ describe("A ", () => { let context = (await query( new Request("http://localhost/", { signal: new AbortController().signal, - }) + }), )) as StaticHandlerContext; let html = ReactDOMServer.renderToStaticMarkup( @@ -709,7 +709,7 @@ describe("A ", () => { router={createStaticRouter(dataRoutes, context)} context={context} /> - + , ); let expectedJsonString = JSON.stringify( @@ -725,10 +725,10 @@ describe("A ", () => { __type: "RouteErrorResponse", }, }, - }) + }), ); expect(html).toMatch( - `` + ``, ); }); @@ -746,7 +746,7 @@ describe("A ", () => { let context = (await query( new Request("http://localhost/", { signal: new AbortController().signal, - }) + }), )) as StaticHandlerContext; let html = ReactDOMServer.renderToStaticMarkup( @@ -755,7 +755,7 @@ describe("A ", () => { router={createStaticRouter(routes, context)} context={context} /> - + , ); // stack is stripped by default from SSR errors @@ -769,10 +769,10 @@ describe("A ", () => { __type: "Error", }, }, - }) + }), ); expect(html).toMatch( - `` + ``, ); }); @@ -792,7 +792,7 @@ describe("A ", () => { let context = (await query( new Request("http://localhost/", { signal: new AbortController().signal, - }) + }), )) as StaticHandlerContext; let html = ReactDOMServer.renderToStaticMarkup( @@ -801,7 +801,7 @@ describe("A ", () => { router={createStaticRouter(dataRoutes, context)} context={context} /> - + , ); // stack is stripped by default from SSR errors @@ -815,10 +815,10 @@ describe("A ", () => { __type: "Error", }, }, - }) + }), ); expect(html).toMatch( - `` + ``, ); }); @@ -836,7 +836,7 @@ describe("A ", () => { let context = (await query( new Request("http://localhost/", { signal: new AbortController().signal, - }) + }), )) as StaticHandlerContext; let html = ReactDOMServer.renderToStaticMarkup( @@ -845,7 +845,7 @@ describe("A ", () => { router={createStaticRouter(routes, context)} context={context} /> - + , ); // stack is stripped by default from SSR errors @@ -860,10 +860,10 @@ describe("A ", () => { __subType: "ReferenceError", }, }, - }) + }), ); expect(html).toMatch( - `` + ``, ); }); @@ -885,7 +885,7 @@ describe("A ", () => { let context = (await query( new Request("http://localhost/the/path", { signal: new AbortController().signal, - }) + }), )) as StaticHandlerContext; let html = ReactDOMServer.renderToStaticMarkup( @@ -895,7 +895,7 @@ describe("A ", () => { context={context} nonce="nonce-string" /> - + , ); expect(html).toMatch("

👋

"); @@ -904,10 +904,10 @@ describe("A ", () => { loaderData: {}, actionData: null, errors: null, - }) + }), ); expect(html).toMatch( - `` + ``, ); }); @@ -935,7 +935,7 @@ describe("A ", () => { let context = (await query( new Request("http://localhost/the/path", { signal: new AbortController().signal, - }) + }), )) as StaticHandlerContext; let html = ReactDOMServer.renderToStaticMarkup( @@ -945,7 +945,7 @@ describe("A ", () => { context={context} hydrate={false} /> - + , ); expect(html).toMatch("

👋

"); expect(html).not.toMatch("" }; expect(escapeHtml(JSON.stringify(evilObj))).toBe( - '{"evil":"\\u003cscript\\u003e\\u003c/script\\u003e"}' + '{"evil":"\\u003cscript\\u003e\\u003c/script\\u003e"}', ); }); test("with angle brackets should parse back", () => { let evilObj = { evil: "" }; expect(JSON.parse(escapeHtml(JSON.stringify(evilObj)))).toMatchObject( - evilObj + evilObj, ); }); @@ -28,28 +28,28 @@ describe("escapeHtml", () => { test("with ampersands should parse back", () => { let evilObj = { evil: "&" }; expect(JSON.parse(escapeHtml(JSON.stringify(evilObj)))).toMatchObject( - evilObj + evilObj, ); }); test('with "LINE SEPARATOR" and "PARAGRAPH SEPARATOR" should escape', () => { let evilObj = { evil: "\u2028\u2029" }; expect(escapeHtml(JSON.stringify(evilObj))).toBe( - '{"evil":"\\u2028\\u2029"}' + '{"evil":"\\u2028\\u2029"}', ); }); test('with "LINE SEPARATOR" and "PARAGRAPH SEPARATOR" should parse back', () => { let evilObj = { evil: "\u2028\u2029" }; expect(JSON.parse(escapeHtml(JSON.stringify(evilObj)))).toMatchObject( - evilObj + evilObj, ); }); test("escaped line terminators should work", () => { expect(() => { vm.runInNewContext( - "(" + escapeHtml(JSON.stringify({ evil: "\u2028\u2029" })) + ")" + "(" + escapeHtml(JSON.stringify({ evil: "\u2028\u2029" })) + ")", ); }).not.toThrow(); }); diff --git a/packages/react-router/__tests__/server-runtime/responses-test.ts b/packages/react-router/__tests__/server-runtime/responses-test.ts index 44bb4ab8f4..734a751087 100644 --- a/packages/react-router/__tests__/server-runtime/responses-test.ts +++ b/packages/react-router/__tests__/server-runtime/responses-test.ts @@ -18,11 +18,11 @@ describe("json", () => { "Content-Type": "application/json; charset=iso-8859-1", "X-Remix": "is awesome", }, - } + }, ); expect(response.headers.get("Content-Type")).toEqual( - "application/json; charset=iso-8859-1" + "application/json; charset=iso-8859-1", ); expect(response.headers.get("X-Remix")).toEqual("is awesome"); }); diff --git a/packages/react-router/__tests__/server-runtime/server-test.ts b/packages/react-router/__tests__/server-runtime/server-test.ts index 5e88c84f20..2b00f6eb42 100644 --- a/packages/react-router/__tests__/server-runtime/server-test.ts +++ b/packages/react-router/__tests__/server-runtime/server-test.ts @@ -43,7 +43,7 @@ describe("server", () => { handleDocumentRequest(request) { return new Response(`${request.method}, ${request.url} COMPONENT`); }, - } + }, ); describe("createRequestHandler", () => { @@ -72,7 +72,7 @@ describe("server", () => { let response = await handler( new Request(`http://localhost:3000${to}`, { method, - }) + }), ); expect(response.status).toBe(200); @@ -80,7 +80,7 @@ describe("server", () => { expect(text).toContain(method); expect(text).toContain(expected); expect(spy.console).not.toHaveBeenCalled(); - } + }, ); it("strips body for HEAD requests", async () => { @@ -88,7 +88,7 @@ describe("server", () => { let response = await handler( new Request("http://localhost:3000/", { method: "HEAD", - }) + }), ); expect(await response.text()).toBe(""); @@ -107,14 +107,14 @@ describe("server", () => { handleDocumentRequest(request) { return new Response(`${request.method}, ${request.url} COMPONENT`); }, - } + }, ); let handler = createRequestHandler(build); let response = await handler( new Request("http://localhost:3000/_root.data"), { foo: "FOO", - } + }, ); expect(await response.text()).toContain("FOO"); @@ -134,13 +134,13 @@ describe("server", () => { handleDocumentRequest(request) { return new Response(`${request.method}, ${request.url} COMPONENT`); }, - } + }, ); let handler = createRequestHandler(build); let response = await handler( new Request("http://localhost:3000/_root.data"), // @ts-expect-error In apps the expected type is handled via the Future interface - new Map([[fooContext, "FOO"]]) + new Map([[fooContext, "FOO"]]), ); expect(await response.text()).toContain("FOO"); @@ -164,14 +164,14 @@ describe("server", () => { handleDocumentRequest(request) { return new Response(`${request.method}, ${request.url} COMPONENT`); }, - } + }, ); let handler = createRequestHandler(build); let response = await handler( new Request("http://localhost:3000/_root.data"), { foo: "FOO", - } + }, ); expect(response.status).toBe(500); @@ -314,7 +314,7 @@ describe("shared server runtime", () => { let result = await handler(request); expect(await result.text()).toBe( - "Unexpected Server Error\n\nError: should be logged when resource loader throws" + "Unexpected Server Error\n\nError: should be logged when resource loader throws", ); }); @@ -457,7 +457,7 @@ describe("shared server runtime", () => { let result = await handler(request); expect(await result.text()).toBe( - "Unexpected Server Error\n\nError: should be logged when resource loader throws" + "Unexpected Server Error\n\nError: should be logged when resource loader throws", ); }); @@ -505,7 +505,7 @@ describe("shared server runtime", () => { }, { handleError: handleErrorSpy, - } + }, ); let handler = createRequestHandler(build, ServerMode.Test); @@ -526,15 +526,15 @@ describe("shared server runtime", () => { `); expect(handleErrorSpy).toHaveBeenCalledTimes(1); expect(handleErrorSpy.mock.calls[0][0] instanceof DOMException).toBe( - true + true, ); expect(handleErrorSpy.mock.calls[0][0].name).toBe("AbortError"); expect(handleErrorSpy.mock.calls[0][0].message).toBe( - "This operation was aborted" + "This operation was aborted", ); expect(handleErrorSpy.mock.calls[0][1].request.method).toBe("GET"); expect(handleErrorSpy.mock.calls[0][1].request.url).toBe( - "http://test.com/resource" + "http://test.com/resource", ); }); }); @@ -999,7 +999,7 @@ describe("shared server runtime", () => { }, { handleError: handleErrorSpy, - } + }, ); let handler = createRequestHandler(build, ServerMode.Test); @@ -1016,19 +1016,19 @@ describe("shared server runtime", () => { let error = await result.json(); expect(error.message).toBe("This operation was aborted"); expect( - error.stack.startsWith("AbortError: This operation was aborted") + error.stack.startsWith("AbortError: This operation was aborted"), ).toBe(true); expect(handleErrorSpy).toHaveBeenCalledTimes(1); expect(handleErrorSpy.mock.calls[0][0] instanceof DOMException).toBe( - true + true, ); expect(handleErrorSpy.mock.calls[0][0].name).toBe("AbortError"); expect(handleErrorSpy.mock.calls[0][0].message).toBe( - "This operation was aborted" + "This operation was aborted", ); expect(handleErrorSpy.mock.calls[0][1].request.method).toBe("GET"); expect(handleErrorSpy.mock.calls[0][1].request.url).toBe( - "http://test.com/?_data=routes/_index" + "http://test.com/?_data=routes/_index", ); }); }); @@ -1538,7 +1538,7 @@ describe("shared server runtime", () => { expect(context.errors).toBeTruthy(); expect(context.errors!["routes/_index"]).toBeInstanceOf(Error); expect(context.errors!["routes/_index"].message).toBe( - "Unexpected Server Error" + "Unexpected Server Error", ); expect(context.errors!["routes/_index"].stack).toBeUndefined(); expect(context.loaderData).toEqual({ @@ -1680,7 +1680,7 @@ describe("shared server runtime", () => { expect(context.errors).toBeTruthy(); expect(context.errors!["routes/test"]).toBeInstanceOf(Error); expect(context.errors!["routes/test"].message).toBe( - "Unexpected Server Error" + "Unexpected Server Error", ); expect(context.errors!["routes/test"].stack).toBeUndefined(); expect(context.loaderData).toEqual({ @@ -1730,7 +1730,7 @@ describe("shared server runtime", () => { expect(context.errors).toBeTruthy(); expect(context.errors!["routes/_index"]).toBeInstanceOf(Error); expect(context.errors!["routes/_index"].message).toBe( - "Unexpected Server Error" + "Unexpected Server Error", ); expect(context.errors!["routes/_index"].stack).toBeUndefined(); expect(context.loaderData).toEqual({ @@ -1788,7 +1788,7 @@ describe("shared server runtime", () => { expect(context.errors).toBeTruthy(); expect(context.errors!["routes/__layout"]).toBeInstanceOf(Error); expect(context.errors!["routes/__layout"].message).toBe( - "Unexpected Server Error" + "Unexpected Server Error", ); expect(context.errors!["routes/__layout"].stack).toBeUndefined(); expect(context.loaderData).toEqual({ @@ -1846,7 +1846,7 @@ describe("shared server runtime", () => { expect(context.errors).toBeTruthy(); expect(context.errors!["routes/__layout"]).toBeInstanceOf(Error); expect(context.errors!["routes/__layout"].message).toBe( - "Unexpected Server Error" + "Unexpected Server Error", ); expect(context.errors!["routes/__layout"].stack).toBeUndefined(); expect(context.loaderData).toEqual({ @@ -1923,7 +1923,7 @@ describe("shared server runtime", () => { let ogHandleDocumentRequest = build.entry.module.default; build.entry.module.default = function ( _: Request, - responseStatusCode: number + responseStatusCode: number, ) { if (responseStatusCode === 200) { throw new Response("Uh oh!", { @@ -1972,7 +1972,7 @@ describe("shared server runtime", () => { let result = await handler(request); expect(result.status).toBe(500); expect(await result.text()).toBe( - "Unexpected Server Error\n\nError: rofl" + "Unexpected Server Error\n\nError: rofl", ); expect(rootLoader.mock.calls.length).toBe(0); expect(indexLoader.mock.calls.length).toBe(0); @@ -2025,7 +2025,7 @@ describe("shared server runtime", () => { expect(spy.console.mock.calls).toEqual([ [ new Error( - "thrown from handleDocumentRequest and expected to be logged in console only once" + "thrown from handleDocumentRequest and expected to be logged in console only once", ), ], [new Error("second error thrown from handleDocumentRequest")], @@ -2055,7 +2055,7 @@ describe("shared server runtime", () => { }, { handleError: handleErrorSpy, - } + }, ); let handler = createRequestHandler(build, ServerMode.Test); @@ -2073,15 +2073,15 @@ describe("shared server runtime", () => { expect(handleErrorSpy).toHaveBeenCalledTimes(1); expect(handleErrorSpy.mock.calls[0][0] instanceof DOMException).toBe( - true + true, ); expect(handleErrorSpy.mock.calls[0][0].name).toBe("AbortError"); expect(handleErrorSpy.mock.calls[0][0].message).toBe( - "This operation was aborted" + "This operation was aborted", ); expect(handleErrorSpy.mock.calls[0][1].request.method).toBe("GET"); expect(handleErrorSpy.mock.calls[0][1].request.url).toBe( - "http://test.com/" + "http://test.com/", ); }); }); @@ -2113,7 +2113,7 @@ describe("shared server runtime", () => { headers: responseHeaders, }); }, - } + }, ); let handler = createRequestHandler(build, ServerMode.Development); diff --git a/packages/react-router/__tests__/server-runtime/sessions-test.ts b/packages/react-router/__tests__/server-runtime/sessions-test.ts index d45a14a657..dd8a1e15f4 100644 --- a/packages/react-router/__tests__/server-runtime/sessions-test.ts +++ b/packages/react-router/__tests__/server-runtime/sessions-test.ts @@ -103,7 +103,7 @@ describe("Cookie session storage", () => { let setCookie = await commitSession(session); session = await getSession( // Tamper with the session cookie... - getCookieFromSetCookie(setCookie).slice(0, -1) + getCookieFromSetCookie(setCookie).slice(0, -1), ); expect(session.get("user")).toBeUndefined(); @@ -139,7 +139,7 @@ describe("Cookie session storage", () => { let session = await getSession(); let setCookie = await destroySession(session); expect(setCookie).toMatchInlineSnapshot( - `"__session=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax"` + `"__session=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax"`, ); spy.mockRestore(); }); @@ -155,7 +155,7 @@ describe("Cookie session storage", () => { let session = await getSession(); let setCookie = await destroySession(session); expect(setCookie).toMatchInlineSnapshot( - `"__session=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax"` + `"__session=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax"`, ); spy.mockRestore(); }); @@ -173,7 +173,7 @@ describe("Cookie session storage", () => { expect(spy.console).toHaveBeenCalledTimes(1); expect(spy.console).toHaveBeenCalledWith( - 'The "__session" cookie has an "expires" property set. This will cause the expires value to not be updated when the session is committed. Instead, you should set the expires value when serializing the cookie. You can use `commitSession(session, { expires })` if using a session storage object, or `cookie.serialize("value", { expires })` if you\'re using the cookie directly.' + 'The "__session" cookie has an "expires" property set. This will cause the expires value to not be updated when the session is committed. Instead, you should set the expires value when serializing the cookie. You can use `commitSession(session, { expires })` if using a session storage object, or `cookie.serialize("value", { expires })` if you\'re using the cookie directly.', ); }); @@ -182,7 +182,7 @@ describe("Cookie session storage", () => { expect(spy.console).toHaveBeenCalledTimes(1); expect(spy.console).toHaveBeenCalledWith( - 'The "__session" cookie is not signed, but session cookies should be signed to prevent tampering on the client before they are sent back to the server. See https://reactrouter.com/explanation/sessions-and-cookies#signing-cookies for more information.' + 'The "__session" cookie is not signed, but session cookies should be signed to prevent tampering on the client before they are sent back to the server. See https://reactrouter.com/explanation/sessions-and-cookies#signing-cookies for more information.', ); }); }); diff --git a/packages/react-router/__tests__/server-runtime/utils.ts b/packages/react-router/__tests__/server-runtime/utils.ts index 76f4ba77bb..6e4fa4dce5 100644 --- a/packages/react-router/__tests__/server-runtime/utils.ts +++ b/packages/react-router/__tests__/server-runtime/utils.ts @@ -30,7 +30,7 @@ export function mockServerBuild( future?: Partial; handleError?: HandleErrorFunction; handleDocumentRequest?: HandleDocumentRequestFunction; - } = {} + } = {}, ): ServerBuild { return { ssr: true, @@ -87,7 +87,7 @@ export function mockServerBuild( new Response(null, { status: responseStatusCode, headers: responseHeaders, - }) + }), ), handleDataRequest: jest.fn(async (response) => response), handleError: opts.handleError, @@ -113,7 +113,7 @@ export function mockServerBuild( [id]: route, }; }, - {} + {}, ), }; } diff --git a/packages/react-router/__tests__/use-revalidator-test.tsx b/packages/react-router/__tests__/use-revalidator-test.tsx index 973b62dca1..7d3d08f92f 100644 --- a/packages/react-router/__tests__/use-revalidator-test.tsx +++ b/packages/react-router/__tests__/use-revalidator-test.tsx @@ -32,7 +32,7 @@ describe("useRevalidator", () => { loader={async () => `count=${++count}`} element={} /> -
+
, ), { initialEntries: ["/foo"], @@ -41,7 +41,7 @@ describe("useRevalidator", () => { "0-0": "count=1", }, }, - } + }, ); let { container } = render(); @@ -157,14 +157,14 @@ describe("useRevalidator", () => { ); }} /> -
- ) +
, + ), ); let { container } = render(
-
+ , ); fireEvent.click(screen.getByText("/child")); @@ -214,14 +214,14 @@ describe("useRevalidator", () => { }} Component={() =>

{("Child:" + useLoaderData()) as string}

} /> -
- ) + , + ), ); let { container } = render(
-
+ , ); fireEvent.click(screen.getByText("/child")); @@ -312,7 +312,7 @@ describe("useRevalidator", () => { hydrationData: { loaderData: { root: 0 }, }, - } + }, ); render(); diff --git a/packages/react-router/__tests__/useHref-basename-test.tsx b/packages/react-router/__tests__/useHref-basename-test.tsx index 0b5bde505f..866a36a658 100644 --- a/packages/react-router/__tests__/useHref-basename-test.tsx +++ b/packages/react-router/__tests__/useHref-basename-test.tsx @@ -16,7 +16,7 @@ describe("useHref under a basename", () => { } /> - + , ); }); @@ -37,7 +37,7 @@ describe("useHref under a basename", () => { } /> - + , ); }); @@ -57,7 +57,7 @@ describe("useHref under a basename", () => { } /> - + , ); }); @@ -78,7 +78,7 @@ describe("useHref under a basename", () => { } /> - + , ); }); @@ -100,7 +100,7 @@ describe("useHref under a basename", () => { } /> - + , ); }); @@ -120,7 +120,7 @@ describe("useHref under a basename", () => { } /> - + , ); }); @@ -141,7 +141,7 @@ describe("useHref under a basename", () => { } /> - + , ); }); @@ -163,7 +163,7 @@ describe("useHref under a basename", () => { } /> - + , ); }); @@ -183,7 +183,7 @@ describe("useHref under a basename", () => { } /> - + , ); }); @@ -204,7 +204,7 @@ describe("useHref under a basename", () => { } /> - + , ); }); @@ -229,7 +229,7 @@ describe("useHref under a basename", () => { element={} /> - + , ); }); @@ -253,7 +253,7 @@ describe("useHref under a basename", () => { } /> - + , ); }); @@ -281,7 +281,7 @@ describe("useHref under a basename", () => { element={} /> - + , ); }); @@ -309,7 +309,7 @@ describe("useHref under a basename", () => { element={} /> - + , ); }); diff --git a/packages/react-router/__tests__/useHref-test.tsx b/packages/react-router/__tests__/useHref-test.tsx index f3e2c4c173..0aa3c23bd0 100644 --- a/packages/react-router/__tests__/useHref-test.tsx +++ b/packages/react-router/__tests__/useHref-test.tsx @@ -19,7 +19,7 @@ describe("useHref", () => { element={} /> - + , ); }); @@ -42,7 +42,7 @@ describe("useHref", () => { element={} /> - + , ); }); @@ -66,7 +66,7 @@ describe("useHref", () => { element={} /> - + , ); }); @@ -88,7 +88,7 @@ describe("useHref", () => { } /> - + , ); }); @@ -108,7 +108,7 @@ describe("useHref", () => { } /> - + , ); }); @@ -129,7 +129,7 @@ describe("useHref", () => { } /> - + , ); }); @@ -153,7 +153,7 @@ describe("useHref", () => { } /> - + , ); }); @@ -175,7 +175,7 @@ describe("useHref", () => { } /> - + , ); }); @@ -201,7 +201,7 @@ describe("useHref", () => { /> - + , ); }); @@ -226,7 +226,7 @@ describe("useHref", () => { element={} /> - + , ); }); @@ -252,7 +252,7 @@ describe("useHref", () => { /> - + , ); }); @@ -272,7 +272,7 @@ describe("useHref", () => { } /> - + , ); }); diff --git a/packages/react-router/__tests__/useLocation-test.tsx b/packages/react-router/__tests__/useLocation-test.tsx index 121073a0d5..de87981e74 100644 --- a/packages/react-router/__tests__/useLocation-test.tsx +++ b/packages/react-router/__tests__/useLocation-test.tsx @@ -16,7 +16,7 @@ describe("useLocation", () => { } /> - + , ); }); @@ -33,7 +33,7 @@ describe("useLocation", () => { renderer = TestRenderer.create( - + , ); }); @@ -74,7 +74,7 @@ describe("useLocation", () => { } /> - + , ); }); diff --git a/packages/react-router/__tests__/useMatch-test.tsx b/packages/react-router/__tests__/useMatch-test.tsx index f51219a055..21dcbe6d53 100644 --- a/packages/react-router/__tests__/useMatch-test.tsx +++ b/packages/react-router/__tests__/useMatch-test.tsx @@ -19,7 +19,7 @@ describe("useMatch", () => { Home} /> - + , ); }); @@ -51,7 +51,7 @@ describe("useMatch", () => { Home} /> - + , ); }); @@ -83,7 +83,7 @@ describe("useMatch", () => { Home} /> - + , ); }); @@ -118,7 +118,7 @@ describe("useMatch", () => { } /> - + , ); }); @@ -128,7 +128,7 @@ describe("useMatch", () => { } /> - + , ); }); diff --git a/packages/react-router/__tests__/useNavigate-test.tsx b/packages/react-router/__tests__/useNavigate-test.tsx index 66823db747..743aed2709 100644 --- a/packages/react-router/__tests__/useNavigate-test.tsx +++ b/packages/react-router/__tests__/useNavigate-test.tsx @@ -38,7 +38,7 @@ describe("useNavigate", () => { } /> About} /> - + , ); }); @@ -74,7 +74,7 @@ describe("useNavigate", () => { } /> - + , ); }); @@ -131,7 +131,7 @@ describe("useNavigate", () => { } /> - + , ); }); @@ -188,7 +188,7 @@ describe("useNavigate", () => { } /> - + , ); }); @@ -264,40 +264,40 @@ describe("useNavigate", () => { } /> - + , ); }); expect(() => TestRenderer.act(() => { renderer.root.findAllByType("button")[0].props.onClick(); - }) + }), ).toThrowErrorMatchingInlineSnapshot( - `"Cannot include a '?' character in a manually specified \`to.pathname\` field [{"pathname":"/about/thing?search"}]. Please separate it out to the \`to.search\` field. Alternatively you may provide the full path as a string in and the router will parse it for you."` + `"Cannot include a '?' character in a manually specified \`to.pathname\` field [{"pathname":"/about/thing?search"}]. Please separate it out to the \`to.search\` field. Alternatively you may provide the full path as a string in and the router will parse it for you."`, ); expect(() => TestRenderer.act(() => { renderer.root.findAllByType("button")[1].props.onClick(); - }) + }), ).toThrowErrorMatchingInlineSnapshot( - `"Cannot include a '#' character in a manually specified \`to.pathname\` field [{"pathname":"/about/thing#hash"}]. Please separate it out to the \`to.hash\` field. Alternatively you may provide the full path as a string in and the router will parse it for you."` + `"Cannot include a '#' character in a manually specified \`to.pathname\` field [{"pathname":"/about/thing#hash"}]. Please separate it out to the \`to.hash\` field. Alternatively you may provide the full path as a string in and the router will parse it for you."`, ); expect(() => TestRenderer.act(() => { renderer.root.findAllByType("button")[2].props.onClick(); - }) + }), ).toThrowErrorMatchingInlineSnapshot( - `"Cannot include a '?' character in a manually specified \`to.pathname\` field [{"pathname":"/about/thing?search#hash"}]. Please separate it out to the \`to.search\` field. Alternatively you may provide the full path as a string in and the router will parse it for you."` + `"Cannot include a '?' character in a manually specified \`to.pathname\` field [{"pathname":"/about/thing?search#hash"}]. Please separate it out to the \`to.search\` field. Alternatively you may provide the full path as a string in and the router will parse it for you."`, ); expect(() => TestRenderer.act(() => { renderer.root.findAllByType("button")[3].props.onClick(); - }) + }), ).toThrowErrorMatchingInlineSnapshot( - `"Cannot include a '#' character in a manually specified \`to.search\` field [{"pathname":"/about/thing","search":"?search#hash"}]. Please separate it out to the \`to.hash\` field. Alternatively you may provide the full path as a string in and the router will parse it for you."` + `"Cannot include a '#' character in a manually specified \`to.search\` field [{"pathname":"/about/thing","search":"?search#hash"}]. Please separate it out to the \`to.hash\` field. Alternatively you may provide the full path as a string in and the router will parse it for you."`, ); }); @@ -493,7 +493,7 @@ describe("useNavigate", () => { } /> About} /> - + , ); }); @@ -510,7 +510,7 @@ describe("useNavigate", () => { `); expect(warnSpy).toHaveBeenCalledWith( - "You should call navigate() in a React.useEffect(), not when your component is first rendered." + "You should call navigate() in a React.useEffect(), not when your component is first rendered.", ); }); @@ -523,7 +523,7 @@ describe("useNavigate", () => { } /> About} /> - + , ); }); @@ -553,7 +553,7 @@ describe("useNavigate", () => { } /> About} /> - + , ); }); @@ -561,7 +561,7 @@ describe("useNavigate", () => { let navigate = useNavigate(); let onChildRendered = React.useCallback( () => navigate("/about"), - [navigate] + [navigate], ); return ; } @@ -610,7 +610,7 @@ describe("useNavigate", () => { `); expect(warnSpy).toHaveBeenCalledWith( - "You should call navigate() in a React.useEffect(), not when your component is first rendered." + "You should call navigate() in a React.useEffect(), not when your component is first rendered.", ); }); @@ -653,7 +653,7 @@ describe("useNavigate", () => { let navigate = useNavigate(); let onChildRendered = React.useCallback( () => navigate("/about"), - [navigate] + [navigate], ); return ; }, @@ -714,7 +714,7 @@ describe("useNavigate", () => { } /> } /> - + , ); }); @@ -746,7 +746,7 @@ describe("useNavigate", () => { /> About} /> - + , ); }); @@ -776,7 +776,7 @@ describe("useNavigate", () => { /> About} /> - + , ); }); @@ -803,7 +803,7 @@ describe("useNavigate", () => { About} /> - + , ); }); @@ -833,7 +833,7 @@ describe("useNavigate", () => { About} /> - + , ); }); @@ -869,7 +869,7 @@ describe("useNavigate", () => { About} /> - + , ); }); @@ -905,7 +905,7 @@ describe("useNavigate", () => { About} /> - + , ); }); @@ -954,7 +954,7 @@ describe("useNavigate", () => { About} /> - + , ); }); @@ -984,7 +984,7 @@ describe("useNavigate", () => { - + , ); }); @@ -1014,7 +1014,7 @@ describe("useNavigate", () => { element={} /> - + , ); }); @@ -1044,7 +1044,7 @@ describe("useNavigate", () => { /> - + , ); }); @@ -1074,7 +1074,7 @@ describe("useNavigate", () => { /> - + , ); }); @@ -1112,7 +1112,7 @@ describe("useNavigate", () => { - + , ); }); @@ -1150,7 +1150,7 @@ describe("useNavigate", () => { - + , ); }); @@ -1183,7 +1183,7 @@ describe("useNavigate", () => { - + , ); }); @@ -1213,7 +1213,7 @@ describe("useNavigate", () => { } /> - + , ); }); @@ -1268,7 +1268,7 @@ describe("useNavigate", () => { About} /> - + , ); }); @@ -1381,9 +1381,9 @@ describe("useNavigate", () => { <> } /> About} /> - + , ), - { initialEntries: ["/home"] } + { initialEntries: ["/home"] }, ); let renderer: TestRenderer.ReactTestRenderer; @@ -1414,9 +1414,9 @@ describe("useNavigate", () => { element={} /> About} /> - + , ), - { initialEntries: ["/home"] } + { initialEntries: ["/home"] }, ); let renderer: TestRenderer.ReactTestRenderer; @@ -1444,9 +1444,9 @@ describe("useNavigate", () => { } /> About} /> - + , ), - { initialEntries: ["/home"] } + { initialEntries: ["/home"] }, ); let renderer: TestRenderer.ReactTestRenderer; @@ -1477,9 +1477,9 @@ describe("useNavigate", () => { /> About} /> - + , ), - { initialEntries: ["/home"] } + { initialEntries: ["/home"] }, ); let renderer: TestRenderer.ReactTestRenderer; @@ -1516,9 +1516,9 @@ describe("useNavigate", () => { About} /> - + , ), - { initialEntries: ["/home"] } + { initialEntries: ["/home"] }, ); let renderer: TestRenderer.ReactTestRenderer; @@ -1555,9 +1555,9 @@ describe("useNavigate", () => { About} /> - + , ), - { initialEntries: ["/home/page"] } + { initialEntries: ["/home/page"] }, ); let renderer: TestRenderer.ReactTestRenderer; @@ -1607,9 +1607,9 @@ describe("useNavigate", () => { About} /> - + , ), - { initialEntries: ["/home/page"] } + { initialEntries: ["/home/page"] }, ); let renderer: TestRenderer.ReactTestRenderer; @@ -1640,9 +1640,9 @@ describe("useNavigate", () => { Destination} /> - + , ), - { initialEntries: ["/layout/thing"] } + { initialEntries: ["/layout/thing"] }, ); let renderer: TestRenderer.ReactTestRenderer; @@ -1673,9 +1673,9 @@ describe("useNavigate", () => { path="contacts/:id" element={} /> - + , ), - { initialEntries: ["/contacts/1"] } + { initialEntries: ["/contacts/1"] }, ); let renderer: TestRenderer.ReactTestRenderer; @@ -1706,9 +1706,9 @@ describe("useNavigate", () => { element={} /> - + , ), - { initialEntries: ["/contacts/1"] } + { initialEntries: ["/contacts/1"] }, ); let renderer: TestRenderer.ReactTestRenderer; @@ -1739,9 +1739,9 @@ describe("useNavigate", () => { element={} /> - + , ), - { initialEntries: ["/contacts/1"] } + { initialEntries: ["/contacts/1"] }, ); let renderer: TestRenderer.ReactTestRenderer; @@ -1778,9 +1778,9 @@ describe("useNavigate", () => { - + , ), - { initialEntries: ["/contacts/1"] } + { initialEntries: ["/contacts/1"] }, ); let renderer: TestRenderer.ReactTestRenderer; @@ -1817,9 +1817,9 @@ describe("useNavigate", () => { - + , ), - { initialEntries: ["/contacts/1"] } + { initialEntries: ["/contacts/1"] }, ); let renderer: TestRenderer.ReactTestRenderer; @@ -1853,9 +1853,9 @@ describe("useNavigate", () => { Destination} /> - + , ), - { initialEntries: ["/layout/thing"] } + { initialEntries: ["/layout/thing"] }, ); let renderer: TestRenderer.ReactTestRenderer; @@ -1886,9 +1886,9 @@ describe("useNavigate", () => { } /> - + , ), - { initialEntries: ["/contacts/1"] } + { initialEntries: ["/contacts/1"] }, ); function Contacts() { @@ -1951,7 +1951,7 @@ describe("useNavigate", () => { ], }, ], - { initialEntries: ["/home"] } + { initialEntries: ["/home"] }, ); let renderer: TestRenderer.ReactTestRenderer; @@ -2071,7 +2071,7 @@ describe("useNavigate", () => { } /> Path} /> - + , ); }); @@ -2115,7 +2115,7 @@ describe("useNavigate", () => { /> Path} /> - + , ); }); @@ -2154,7 +2154,7 @@ describe("useNavigate", () => { }, { path: "/path", Component: () =>

Path

}, ], - { basename: "/base", initialEntries: ["/base"] } + { basename: "/base", initialEntries: ["/base"] }, ); function Home() { @@ -2201,7 +2201,7 @@ describe("useNavigate", () => { }, { path: "/path", Component: () =>

Path

}, ], - { basename: "/base", initialEntries: ["/base"] } + { basename: "/base", initialEntries: ["/base"] }, ); function Home() { diff --git a/packages/react-router/__tests__/useOutlet-test.tsx b/packages/react-router/__tests__/useOutlet-test.tsx index f283bd4874..32dc725d96 100644 --- a/packages/react-router/__tests__/useOutlet-test.tsx +++ b/packages/react-router/__tests__/useOutlet-test.tsx @@ -22,7 +22,7 @@ describe("useOutlet", () => { } /> - + , ); }); @@ -42,7 +42,7 @@ describe("useOutlet", () => { } /> - + , ); }); @@ -66,7 +66,7 @@ describe("useOutlet", () => { } /> - + , ); }); @@ -93,7 +93,7 @@ describe("useOutlet", () => { Profile} /> - + , ); }); @@ -119,7 +119,7 @@ describe("useOutlet", () => { index} /> - + , ); }); @@ -145,7 +145,7 @@ describe("useOutlet", () => { index} /> - + , ); }); @@ -183,7 +183,7 @@ describe("useOutlet", () => { } /> - + , ); }); @@ -235,7 +235,7 @@ describe("useOutlet", () => { } /> - + , ); }); @@ -286,7 +286,7 @@ describe("useOutlet", () => {
- + , ); }); @@ -328,7 +328,7 @@ describe("useOutlet", () => {
- + , ); }); diff --git a/packages/react-router/__tests__/useParams-test.tsx b/packages/react-router/__tests__/useParams-test.tsx index 4b34daf16a..5413cee245 100644 --- a/packages/react-router/__tests__/useParams-test.tsx +++ b/packages/react-router/__tests__/useParams-test.tsx @@ -14,7 +14,7 @@ describe("useParams", () => { renderer = TestRenderer.create( - + , ); }); @@ -35,7 +35,7 @@ describe("useParams", () => { } /> - + , ); }); @@ -56,7 +56,7 @@ describe("useParams", () => { } /> - + , ); }); @@ -89,7 +89,7 @@ describe("useParams", () => { } /> - + , ); }); @@ -116,7 +116,7 @@ describe("useParams", () => { } /> - + , ); }); @@ -137,7 +137,7 @@ describe("useParams", () => { } /> - + , ); }); @@ -171,7 +171,7 @@ describe("useParams", () => { } /> - + , ); }); @@ -182,7 +182,7 @@ describe("useParams", () => { `); expect(consoleWarn).toHaveBeenCalledWith( - expect.stringMatching("malformed URL segment") + expect.stringMatching("malformed URL segment"), ); }); }); @@ -198,7 +198,7 @@ describe("useParams", () => { - + , ); }); diff --git a/packages/react-router/__tests__/useResolvedPath-test.tsx b/packages/react-router/__tests__/useResolvedPath-test.tsx index 7f183b4f7a..785694708e 100644 --- a/packages/react-router/__tests__/useResolvedPath-test.tsx +++ b/packages/react-router/__tests__/useResolvedPath-test.tsx @@ -26,7 +26,7 @@ describe("useResolvedPath", () => { element={} /> - + , ); }); @@ -56,7 +56,7 @@ describe("useResolvedPath", () => { } /> - + , ); }); @@ -79,7 +79,7 @@ describe("useResolvedPath", () => { element={} /> - + , ); }); @@ -102,7 +102,7 @@ describe("useResolvedPath", () => { } /> - + , ); }); @@ -123,7 +123,7 @@ describe("useResolvedPath", () => { } /> - + , ); }); @@ -149,7 +149,7 @@ describe("useResolvedPath", () => { } /> - + , ); }); @@ -175,7 +175,7 @@ describe("useResolvedPath", () => { } /> - + , ); }); @@ -198,7 +198,7 @@ describe("useResolvedPath", () => { } /> - + , ); }); @@ -219,7 +219,7 @@ describe("useResolvedPath", () => { } /> - + , ); }); @@ -381,7 +381,7 @@ describe("useResolvedPath", () => { - + , ); let html = getHtml(container); html = html ? html.replace(/</g, "<").replace(/>/g, ">") : html; diff --git a/packages/react-router/__tests__/useRoutes-test.tsx b/packages/react-router/__tests__/useRoutes-test.tsx index 91de38a632..b433e986b9 100644 --- a/packages/react-router/__tests__/useRoutes-test.tsx +++ b/packages/react-router/__tests__/useRoutes-test.tsx @@ -15,7 +15,7 @@ describe("useRoutes", () => { renderer = TestRenderer.create( - + , ); }); @@ -41,7 +41,7 @@ describe("useRoutes", () => { renderer = TestRenderer.create( - + , ); }); @@ -64,7 +64,7 @@ describe("useRoutes", () => { renderer = TestRenderer.create( - + , ); }); @@ -90,7 +90,7 @@ describe("useRoutes", () => { renderer = TestRenderer.create( - + , ); }); @@ -124,7 +124,7 @@ describe("useRoutes", () => { routes={routes} location={{ pathname: "/three", search: "", hash: "" }} /> - + , ); }); @@ -159,15 +159,15 @@ describe("useRoutes", () => { TestRenderer.create( - + , ); }); expect(consoleWarn).toHaveBeenCalledTimes(1); expect(consoleWarn).toHaveBeenCalledWith( expect.stringContaining( - `Matched leaf route at location "/layout" does not have an element` - ) + `Matched leaf route at location "/layout" does not have an element`, + ), ); }); }); diff --git a/packages/react-router/__tests__/utils/MemoryNavigate.tsx b/packages/react-router/__tests__/utils/MemoryNavigate.tsx index 146e059011..a1c0a31d30 100644 --- a/packages/react-router/__tests__/utils/MemoryNavigate.tsx +++ b/packages/react-router/__tests__/utils/MemoryNavigate.tsx @@ -25,7 +25,7 @@ export default function MemoryNavigate({ dataRouterContext?.router.navigate(to); } }, - [dataRouterContext, to, formMethod, formData] + [dataRouterContext, to, formMethod, formData], ); // Only prepend the basename to the rendered href, send the non-prefixed `to` diff --git a/packages/react-router/__tests__/utils/framework.ts b/packages/react-router/__tests__/utils/framework.ts index 824fc3a761..72762d49d7 100644 --- a/packages/react-router/__tests__/utils/framework.ts +++ b/packages/react-router/__tests__/utils/framework.ts @@ -4,7 +4,7 @@ import type { } from "../../lib/dom/ssr/entry"; export function mockFrameworkContext( - overrides?: Partial + overrides?: Partial, ): FrameworkContextObject { return { routeModules: { root: { default: () => null } }, @@ -44,7 +44,7 @@ export function mockFrameworkContext( } export function mockEntryContext( - overrides?: Partial + overrides?: Partial, ): EntryContext { return { ...mockFrameworkContext(overrides), diff --git a/packages/react-router/__tests__/utils/renderStrict.tsx b/packages/react-router/__tests__/utils/renderStrict.tsx index 3ce64e06c4..a9f4d19483 100644 --- a/packages/react-router/__tests__/utils/renderStrict.tsx +++ b/packages/react-router/__tests__/utils/renderStrict.tsx @@ -13,7 +13,7 @@ function renderStrict( element: | React.FunctionComponentElement | React.FunctionComponentElement[], - node: ReactDOM.Container + node: ReactDOM.Container, ): void { ReactDOM.render({element}, node); } diff --git a/packages/react-router/__tests__/vendor/turbo-stream-test.ts b/packages/react-router/__tests__/vendor/turbo-stream-test.ts index 54332b1703..8c9d615e55 100644 --- a/packages/react-router/__tests__/vendor/turbo-stream-test.ts +++ b/packages/react-router/__tests__/vendor/turbo-stream-test.ts @@ -244,7 +244,7 @@ test("should encode and decode object and dedupe object key, value, and promise write(chunk) { encoded += chunk; }, - }) + }), ); expect(Array.from(encoded.matchAll(/"foo"/g))).toHaveLength(1); @@ -288,7 +288,7 @@ test("should encode and decode rejected promise", async () => { const decoded = await decode(encode(input)); expect(decoded.value).toBeInstanceOf(Promise); await expect(decoded.value).rejects.toEqual( - await input.catch((reason) => reason) + await input.catch((reason) => reason), ); await decoded.done; }); @@ -308,7 +308,7 @@ test("should encode and decode object with rejected promise", async () => { const value = decoded.value as typeof input; expect(value.foo).toBeInstanceOf(Promise); await expect(value.foo).rejects.toEqual( - await input.foo.catch((reason) => reason) + await input.foo.catch((reason) => reason), ); return decoded.done; }); @@ -352,7 +352,7 @@ test("should encode and decode custom type", async () => { }), { plugins: [decoder], - } + }, ); const value = decoded.value as Custom; expect(value).toBeInstanceOf(Custom); @@ -393,7 +393,7 @@ test("should encode and decode custom type when nested alongside Promise", async } }, ], - } + }, )) as unknown as { value: { number: number; @@ -431,7 +431,7 @@ test("should allow plugins to encode and decode functions", async () => { } }, ], - } + }, ); expect(decoded.value).toBeInstanceOf(Function); expect((decoded.value as typeof input)()).toBe("foo"); @@ -460,7 +460,7 @@ test("should allow postPlugins to handle values that would otherwise throw", asy } }, ], - } + }, ); expect(decoded.value).toEqual({ func: undefined, class: undefined }); await decoded.done; @@ -471,7 +471,7 @@ test("should propagate abort reason to deferred promises for sync resolved promi const reason = new Error("reason"); abortController.abort(reason); const decoded = await decode( - encode(Promise.resolve("foo"), { signal: abortController.signal }) + encode(Promise.resolve("foo"), { signal: abortController.signal }), ); await expect(decoded.value).rejects.toEqual(reason); }); @@ -481,7 +481,7 @@ test("should propagate abort reason to deferred promises for async resolved prom const deferred = new Deferred(); const reason = new Error("reason"); const decoded = await decode( - encode(deferred.promise, { signal: abortController.signal }) + encode(deferred.promise, { signal: abortController.signal }), ); abortController.abort(reason); await expect(decoded.value).rejects.toEqual(reason); @@ -511,7 +511,7 @@ test("should encode and decode objects with multiple promises resolving to the s write(chunk) { encoded += chunk; }, - }) + }), ); expect(Array.from(encoded.matchAll(/"baz"/g))).toHaveLength(1); }); @@ -542,7 +542,7 @@ test("should encode and decode objects with reused values", async () => { write(chunk) { encoded += chunk; }, - }) + }), ); expect(Array.from(encoded.matchAll(/"baz"/g))).toHaveLength(1); await decoded.done; @@ -573,7 +573,7 @@ test("should encode and decode objects with multiple promises rejecting to the s write(chunk) { encoded += chunk; }, - }) + }), ); expect(Array.from(encoded.matchAll(/"baz"/g))).toHaveLength(1); }); diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index f28fe09314..0e975bf676 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -85,7 +85,7 @@ export function mapRouteProperties(route: RouteObject) { warning( false, "You should not include both `Component` and `element` on your route - " + - "`Component` will be used." + "`Component` will be used.", ); } } @@ -101,7 +101,7 @@ export function mapRouteProperties(route: RouteObject) { warning( false, "You should not include both `HydrateFallback` and `hydrateFallbackElement` on your route - " + - "`HydrateFallback` will be used." + "`HydrateFallback` will be used.", ); } } @@ -117,7 +117,7 @@ export function mapRouteProperties(route: RouteObject) { warning( false, "You should not include both `ErrorBoundary` and `errorElement` on your route - " + - "`ErrorBoundary` will be used." + "`ErrorBoundary` will be used.", ); } } @@ -196,7 +196,7 @@ export interface MemoryRouterOpts { */ export function createMemoryRouter( routes: RouteObject[], - opts?: MemoryRouterOpts + opts?: MemoryRouterOpts, ): DataRouter { return createRouter({ basename: opts?.basename, @@ -303,7 +303,7 @@ export function RouterProvider({ let setState = React.useCallback( ( newState: RouterState, - { deletedFetchers, flushSync, viewTransitionOpts } + { deletedFetchers, flushSync, viewTransitionOpts }, ) => { newState.fetchers.forEach((fetcher, key) => { if (fetcher.data !== undefined) { @@ -319,7 +319,7 @@ export function RouterProvider({ "so `ReactDOM.flushSync()` is unavailable. Please update your app " + 'to `import { RouterProvider } from "react-router/dom"` and ensure ' + "you have `react-dom` installed as a dependency to use the " + - "`flushSync` option." + "`flushSync` option.", ); let isViewTransitionAvailable = @@ -331,7 +331,7 @@ export function RouterProvider({ viewTransitionOpts == null || isViewTransitionAvailable, "You provided the `viewTransition` option to a router update, " + "but you do not appear to be running in a DOM environment as " + - "`window.startViewTransition` is not available." + "`window.startViewTransition` is not available.", ); // If this isn't a view transition or it's not available in this browser, @@ -403,7 +403,7 @@ export function RouterProvider({ }); } }, - [router.window, reactDomFlushSyncImpl, transition, renderDfd] + [router.window, reactDomFlushSyncImpl, transition, renderDfd], ); // Need to use a layout effect here so we are subscribed early enough to @@ -494,7 +494,7 @@ export function RouterProvider({ static: false, basename, }), - [router, navigator, basename] + [router, navigator, basename], ); // The fragment and {null} here are important! We need them to keep React 18's @@ -604,7 +604,7 @@ export function MemoryRouter({ (newState: { action: NavigationType; location: Location }) => { React.startTransition(() => setStateImpl(newState)); }, - [setStateImpl] + [setStateImpl], ); React.useLayoutEffect(() => history.listen(setState), [history, setState]); @@ -672,7 +672,7 @@ export function Navigate({ useInRouterContext(), // TODO: This error is probably because they somehow have 2 versions of // the router loaded. We can help them understand how to avoid that. - ` may be used only in the context of a component.` + ` may be used only in the context of a component.`, ); let { static: isStatic } = React.useContext(NavigationContext); @@ -681,7 +681,7 @@ export function Navigate({ !isStatic, ` must not be used on the initial render in a . ` + `This is a no-op, but you should modify your code so the is ` + - `only ever rendered in response to some user interaction or state change.` + `only ever rendered in response to some user interaction or state change.`, ); let { matches } = React.useContext(RouteContext); @@ -694,7 +694,7 @@ export function Navigate({ to, getResolveToMatches(matches), locationPathname, - relative === "path" + relative === "path", ); let jsonPath = JSON.stringify(path); @@ -977,7 +977,7 @@ export function Route(props: RouteProps): React.ReactElement | null { invariant( false, `A is only ever to be used as the child of element, ` + - `never rendered directly. Please wrap your in a .` + `never rendered directly. Please wrap your in a .`, ); } @@ -1045,7 +1045,7 @@ export function Router({ invariant( !useInRouterContext(), `You cannot render a inside another .` + - ` You should never have more than one in your app.` + ` You should never have more than one in your app.`, ); // Preserve trailing slashes on basename, so we can let the user control @@ -1058,7 +1058,7 @@ export function Router({ static: staticProp, future: {}, }), - [basename, navigator, staticProp] + [basename, navigator, staticProp], ); if (typeof locationProp === "string") { @@ -1096,7 +1096,7 @@ export function Router({ locationContext != null, ` is not able to match the URL ` + `"${pathname}${search}${hash}" because it does not start with the ` + - `basename, so the won't render anything.` + `basename, so the won't render anything.`, ); if (locationContext == null) { @@ -1352,7 +1352,7 @@ class AwaitErrorBoundary extends React.Component< console.error( " caught the following error during render", error, - errorInfo + errorInfo, ); } @@ -1382,8 +1382,8 @@ class AwaitErrorBoundary extends React.Component< "_error" in promise ? AwaitRenderStatus.error : "_data" in promise - ? AwaitRenderStatus.success - : AwaitRenderStatus.pending; + ? AwaitRenderStatus.success + : AwaitRenderStatus.pending; } else { // Raw (untracked) promise - track it status = AwaitRenderStatus.pending; @@ -1392,7 +1392,7 @@ class AwaitErrorBoundary extends React.Component< (data: any) => Object.defineProperty(resolve, "_data", { get: () => data }), (error: any) => - Object.defineProperty(resolve, "_error", { get: () => error }) + Object.defineProperty(resolve, "_error", { get: () => error }), ); } @@ -1444,7 +1444,7 @@ function ResolveAwait({ */ export function createRoutesFromChildren( children: React.ReactNode, - parentPath: number[] = [] + parentPath: number[] = [], ): RouteObject[] { let routes: RouteObject[] = []; @@ -1461,7 +1461,7 @@ export function createRoutesFromChildren( // Transparently support React.Fragment and its children. routes.push.apply( routes, - createRoutesFromChildren(element.props.children, treePath) + createRoutesFromChildren(element.props.children, treePath), ); return; } @@ -1470,12 +1470,12 @@ export function createRoutesFromChildren( element.type === Route, `[${ typeof element.type === "string" ? element.type : element.type.name - }] is not a component. All component children of must be a or ` + }] is not a component. All component children of must be a or `, ); invariant( !element.props.index || !element.props.children, - "An index route cannot have child routes." + "An index route cannot have child routes.", ); let route: RouteObject = { @@ -1503,7 +1503,7 @@ export function createRoutesFromChildren( if (element.props.children) { route.children = createRoutesFromChildren( element.props.children, - treePath + treePath, ); } @@ -1552,7 +1552,7 @@ export let createRoutesFromElements = createRoutesFromChildren; * @returns A React element that renders the matched routes, or null if no matches */ export function renderMatches( - matches: RouteMatch[] | null + matches: RouteMatch[] | null, ): React.ReactElement | null { return _renderMatches(matches); } diff --git a/packages/react-router/lib/context.ts b/packages/react-router/lib/context.ts index 9ff5c96891..21b9c380a4 100644 --- a/packages/react-router/lib/context.ts +++ b/packages/react-router/lib/context.ts @@ -73,7 +73,7 @@ export type DataRouteObject = RouteObject & { export interface RouteMatch< ParamKey extends string = string, - RouteObjectType extends RouteObject = RouteObject + RouteObjectType extends RouteObject = RouteObject, > extends AgnosticRouteMatch {} export interface DataRouteMatch extends RouteMatch {} @@ -128,7 +128,7 @@ ViewTransitionContext.displayName = "ViewTransition"; export type FetchersContextObject = Map; export const FetchersContext = React.createContext( - new Map() + new Map(), ); FetchersContext.displayName = "Fetchers"; @@ -178,7 +178,7 @@ interface NavigationContextObject { } export const NavigationContext = React.createContext( - null! + null!, ); NavigationContext.displayName = "Navigation"; @@ -188,7 +188,7 @@ interface LocationContextObject { } export const LocationContext = React.createContext( - null! + null!, ); LocationContext.displayName = "Location"; diff --git a/packages/react-router/lib/dom-export/hydrated-router.tsx b/packages/react-router/lib/dom-export/hydrated-router.tsx index eb2ede42e7..ef01937cf5 100644 --- a/packages/react-router/lib/dom-export/hydrated-router.tsx +++ b/packages/react-router/lib/dom-export/hydrated-router.tsx @@ -55,7 +55,7 @@ function initSsrInfo(): void { if (importMap?.textContent) { try { window.__reactRouterManifest.sri = JSON.parse( - importMap.textContent + importMap.textContent, ).integrity; } catch (err) { console.error("Failed to parse import map", err); @@ -84,7 +84,7 @@ function createHydratedRouter({ if (!ssrInfo) { throw new Error( "You must be using the SSR features of React Router in order to skip " + - "passing a `router` prop to ``" + "passing a `router` prop to ``", ); } @@ -121,7 +121,7 @@ function createHydratedRouter({ ssrInfo.routeModules, ssrInfo.context.state, ssrInfo.context.ssr, - ssrInfo.context.isSpaMode + ssrInfo.context.isSpaMode, ); let hydrationData: HydrationState | undefined = undefined; @@ -151,7 +151,7 @@ function createHydratedRouter({ }), window.location, window.__reactRouterContext?.basename, - ssrInfo.context.isSpaMode + ssrInfo.context.isSpaMode, ); if (hydrationData && hydrationData.errors) { @@ -179,7 +179,7 @@ function createHydratedRouter({ ssrInfo.manifest, ssrInfo.routeModules, ssrInfo.context.ssr, - ssrInfo.context.basename + ssrInfo.context.basename, ), patchRoutesOnNavigation: getPatchRoutesOnNavigationFunction( ssrInfo.manifest, @@ -187,7 +187,7 @@ function createHydratedRouter({ ssrInfo.context.ssr, ssrInfo.context.routeDiscovery, ssrInfo.context.isSpaMode, - ssrInfo.context.basename + ssrInfo.context.basename, ), }); ssrInfo.router = router; @@ -235,7 +235,7 @@ export function HydratedRouter(props: HydratedRouterProps) { let [criticalCss, setCriticalCss] = React.useState( process.env.NODE_ENV === "development" ? ssrInfo?.context.criticalCss - : undefined + : undefined, ); React.useEffect(() => { if (process.env.NODE_ENV === "development") { @@ -272,7 +272,7 @@ export function HydratedRouter(props: HydratedRouterProps) { ssrInfo.routeModules, ssrInfo.context.ssr, ssrInfo.context.routeDiscovery, - ssrInfo.context.isSpaMode + ssrInfo.context.isSpaMode, ); // We need to include a wrapper RemixErrorBoundary here in case the root error diff --git a/packages/react-router/lib/dom/dom.ts b/packages/react-router/lib/dom/dom.ts index 24ec0a6945..82404939fb 100644 --- a/packages/react-router/lib/dom/dom.ts +++ b/packages/react-router/lib/dom/dom.ts @@ -33,7 +33,7 @@ function isModifiedEvent(event: LimitedMouseEvent) { export function shouldProcessLinkClick( event: LimitedMouseEvent, - target?: string + target?: string, ) { return ( event.button === 0 && // Ignore everything but left clicks @@ -77,7 +77,7 @@ export type URLSearchParamsInit = @category Utils */ export function createSearchParams( - init: URLSearchParamsInit = "" + init: URLSearchParamsInit = "", ): URLSearchParams { return new URLSearchParams( typeof init === "string" || @@ -87,15 +87,15 @@ export function createSearchParams( : Object.keys(init).reduce((memo, key) => { let value = init[key]; return memo.concat( - Array.isArray(value) ? value.map((v) => [key, v]) : [[key, value]] + Array.isArray(value) ? value.map((v) => [key, v]) : [[key, value]], ); - }, [] as ParamKeyValuePair[]) + }, [] as ParamKeyValuePair[]), ); } export function getSearchParamsForLocation( locationSearch: string, - defaultSearchParams: URLSearchParams | null + defaultSearchParams: URLSearchParams | null, ) { let searchParams = createSearchParams(locationSearch); @@ -143,7 +143,7 @@ function isFormDataSubmitterSupported() { new FormData( document.createElement("form"), // @ts-expect-error if FormData supports the submitter parameter, this will throw - 0 + 0, ); _formDataSupportsSubmitter = false; } catch (e) { @@ -242,7 +242,7 @@ function getFormEncType(encType: string | null) { warning( false, `"${encType}" is not a valid \`encType\` for \`
\`/\`\` ` + - `and will default to "${defaultEncType}"` + `and will default to "${defaultEncType}"`, ); return null; @@ -252,7 +252,7 @@ function getFormEncType(encType: string | null) { export function getFormSubmissionInfo( target: SubmitTarget, - basename: string + basename: string, ): { action: string | null; method: string; @@ -285,7 +285,7 @@ export function getFormSubmissionInfo( if (form == null) { throw new Error( - `Cannot submit a