diff --git a/docs/components/form.md b/docs/components/form.md index 874a338369..b062dec138 100644 --- a/docs/components/form.md +++ b/docs/components/form.md @@ -7,6 +7,8 @@ new: true The Form component is a wrapper around a plain HTML [form][htmlform] that emulates the browser for client side routing and data mutations. It is _not_ a form validation/state management library like you might be used to in the React ecosystem (for that, we recommend the browser's built in [HTML Form Validation][formvalidation] and data validation on your backend server). +This feature only works if using a data router, see [Picking a Router][pickingarouter] + ```tsx import { Form } from "react-router-dom"; @@ -311,3 +313,4 @@ You can access those values from the `request.url` [remix]: https://remix.run [formvalidation]: https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation [indexsearchparam]: ../guides/index-search-param +[pickingarouter]: ../routers/picking-a-router diff --git a/docs/components/scroll-restoration.md b/docs/components/scroll-restoration.md index 015728c07c..5f0df97da2 100644 --- a/docs/components/scroll-restoration.md +++ b/docs/components/scroll-restoration.md @@ -7,6 +7,8 @@ new: true This component will emulate the browser's scroll restoration on location changes after loaders have completed to ensure the scroll position is restored to the right spot, even across domains. +This feature only works if using a data router, see [Picking a Router][pickingarouter] + You should only render one of these and it's recommended you render it in the root route of your app: ```tsx [1,7] @@ -97,3 +99,4 @@ Server Rendering frameworks can prevent scroll flashing because they can send a [remix]: https://remix.run [preventscrollreset]: ../components/link#preventscrollreset +[pickingarouter]: ../routers/picking-a-router diff --git a/docs/hooks/use-fetcher.md b/docs/hooks/use-fetcher.md index f8d14452f7..a4359170b7 100644 --- a/docs/hooks/use-fetcher.md +++ b/docs/hooks/use-fetcher.md @@ -7,10 +7,12 @@ new: true In HTML/HTTP, data mutations and loads are modeled with navigation: `` and `
`. Both cause a navigation in the browser. The React Router equivalents are [``][link] and [``][form]. -But sometimes you want to call a loader outside of navigation, or call an action (and get the data on the page to revalidate) without changing the URL. Or you need to have multiple mutations in-flight at the same time. +But sometimes you want to call a [`loader`][loader] outside of navigation, or call an [`action`][action] (and get the data on the page to revalidate) without changing the URL. Or you need to have multiple mutations in-flight at the same time. Many interactions with the server aren't navigation events. This hook lets you plug your UI into your actions and loaders without navigating. +This feature only works if using a data router, see [Picking a Router][pickingarouter] + This is useful when you need to: - fetch data not associated with UI routes (popovers, dynamic forms, etc.) @@ -220,6 +222,9 @@ Tells you the method of the form being submitted: get, post, put, patch, or dele fetcher.formMethod; // "post" ``` +[loader]: ../route/loader +[action]: ../route/action +[pickingarouter]: ../routers/picking-a-router [indexsearchparam]: ../guides/index-search-param [link]: ../components/link [form]: ../components/form diff --git a/docs/hooks/use-fetchers.md b/docs/hooks/use-fetchers.md index b0e9e9edad..dfb19a7b7f 100644 --- a/docs/hooks/use-fetchers.md +++ b/docs/hooks/use-fetchers.md @@ -7,6 +7,8 @@ new: true Returns an array of all inflight [fetchers][usefetcher] without their `load`, `submit`, or `Form` properties (can't have parent components trying to control the behavior of their children! We know from IRL experience that this is a fool's errand.) +This feature only works if using a data router, see [Picking a Router][pickingarouter] + ```tsx import { useFetchers } from "react-router-dom"; @@ -134,3 +136,4 @@ function ProjectTaskCount({ project }) { It's a little bit of work, but it's mostly just asking React Router for the state it's tracking and doing an optimistic calculation based on it. [usefetcher]: ./use-fetcher +[pickingarouter]: ../routers/picking-a-router diff --git a/docs/hooks/use-matches.md b/docs/hooks/use-matches.md index 7ef1b0e1f9..62989bf946 100644 --- a/docs/hooks/use-matches.md +++ b/docs/hooks/use-matches.md @@ -39,7 +39,7 @@ A `match` has the following shape: Pairing `` with `useMatches` gets very powerful since you can put whatever you want on a route `handle` and have access to `useMatches` anywhere. -`useMatches` only works with Data Routers, since they know the full route tree up front and can provide all of the current matches. Additionally, `useMatches` will not match down into any descendant route trees since the router isn't aware of the descendant routes. +This feature only works if using a data router (see [Picking a Router][pickingarouter]) since they know the full route tree up front and can provide all of the current matches. Additionally, `useMatches` will not match down into any descendant route trees since the router isn't aware of the descendant routes. ## Breadcrumbs @@ -98,3 +98,5 @@ function Breadcrumbs() { ``` Now you can render `` anywhere you want, probably in the root component. + +[pickingarouter]: ../routers/picking-a-router diff --git a/docs/hooks/use-navigation.md b/docs/hooks/use-navigation.md index b1f3f36ceb..2d0ad59709 100644 --- a/docs/hooks/use-navigation.md +++ b/docs/hooks/use-navigation.md @@ -13,6 +13,8 @@ This hook tells you everything you need to know about a page navigation to build - Optimistically showing a new record while it's being created on the server - Optimistically showing the new state of a record while it's being updated +This feature only works if using a data router, see [Picking a Router][pickingarouter] + ```js import { useNavigation } from "react-router-dom"; @@ -95,3 +97,4 @@ This tells you what the next [location][location] is going to be. Note that this link will not appear "pending" if a form is being submitted to the URL the link points to, because we only do this for "loading" states. The form will contain the pending UI for when the state is "submitting", once the action is complete, then the link will go pending. [location]: ../utils/location +[pickingarouter]: ../routers/picking-a-router diff --git a/docs/hooks/use-revalidator.md b/docs/hooks/use-revalidator.md index 818c795c20..2be9a51a67 100644 --- a/docs/hooks/use-revalidator.md +++ b/docs/hooks/use-revalidator.md @@ -7,6 +7,8 @@ new: true This hook allows you to revalidate the data for any reason. React Router automatically revalidates the data after actions are called, but you may want to revalidate for other reasons like when focus returns to the window. +This feature only works if using a data router, see [Picking a Router][pickingarouter] + ```tsx import { useRevalidator } from "react-router-dom"; @@ -61,3 +63,4 @@ If a navigation happens while a revalidation is in flight, the revalidation will [form]: ../components/form [usefetcher]: ./use-fetcher [usesubmit]: ./use-submit +[pickingarouter]: ../routers/picking-a-router diff --git a/docs/hooks/use-route-error.md b/docs/hooks/use-route-error.md index 80a7edcf34..377cdcc800 100644 --- a/docs/hooks/use-route-error.md +++ b/docs/hooks/use-route-error.md @@ -7,6 +7,8 @@ new: true Inside of an [`errorElement`][errorelement], this hooks returns anything thrown during an action, loader, or rendering. Note that thrown responses have special treatment, see [`isRouteErrorResponse`][isrouteerrorresponse] for more information. +This feature only works if using a data router, see [Picking a Router][pickingarouter] + ```jsx function ErrorBoundary() { const error = useRouteError(); @@ -33,3 +35,4 @@ function ErrorBoundary() { [errorelement]: ../route/error-element [isrouteerrorresponse]: ../utils/is-route-error-response +[pickingarouter]: ../routers/picking-a-router diff --git a/docs/hooks/use-route-loader-data.md b/docs/hooks/use-route-loader-data.md index cc0001b06c..b2b377174e 100644 --- a/docs/hooks/use-route-loader-data.md +++ b/docs/hooks/use-route-loader-data.md @@ -7,6 +7,8 @@ new: true This hook makes the data at any currently rendered route available anywhere in the tree. This is useful for components deep in the tree needing data from routes much farther up, as well as parent routes needing the data of child routes deeper in the tree. +This feature only works if using a data router, see [Picking a Router][pickingarouter] + ```tsx import { useRouteLoaderData } from "react-router-dom"; diff --git a/docs/hooks/use-submit.md b/docs/hooks/use-submit.md index cc1738b374..ec08da93ad 100644 --- a/docs/hooks/use-submit.md +++ b/docs/hooks/use-submit.md @@ -5,7 +5,11 @@ new: true # `useSubmit` -The imperative version of `` that let's you, the programmer, submit a form instead of the user. For example, submitting the form every time a value changes inside the form: +The imperative version of `` that let's you, the programmer, submit a form instead of the user. + +This feature only works if using a data router, see [Picking a Router][pickingarouter] + +For example, submitting the form every time a value changes inside the form: ```tsx [8] import { useSubmit, Form } from "react-router-dom"; @@ -87,3 +91,5 @@ submit(null, { // same as ; ``` + +[pickingarouter]: ../routers/picking-a-router diff --git a/docs/route/action.md b/docs/route/action.md index 2ce00b0ae2..5863d56e68 100644 --- a/docs/route/action.md +++ b/docs/route/action.md @@ -7,7 +7,7 @@ new: true Route actions are the "writes" to route [loader][loader] "reads". They provide a way for apps to perform data mutations with simple HTML and HTTP semantics while React Router abstracts away the complexity of asynchronous UI and revalidation. This gives you the simple mental model of HTML + HTTP (where the browser handles the asynchrony and revalidation) with the behavior and and UX capabilities of modern SPAs. -This feature only works if using a data router +This feature only works if using a data router, see [Picking a Router][pickingarouter] ```tsx `), the error path will be rendered (``) and the error made available with [`useRouteError`][userouteerror]. -This feature only works if using a data router +This feature only works if using a data router, see [Picking a Router][pickingarouter] ```tsx This feature only works if using a data router, see [Picking a Router][pickingarouter] + There are several instances where data is revalidated, keeping your UI in sync with your data automatically: - After an [`action`][action] is called from a [``][form]. @@ -76,3 +78,4 @@ interface ShouldRevalidateFunction { [loader]: ./loader [useloaderdata]: ../hooks/use-loader-data [params]: ./route#dynamic-segments +[pickingarouter]: ../routers/picking-a-router diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index 6941de6fe6..7994a6b076 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -642,6 +642,35 @@ if (__DEV__) { //#region Hooks //////////////////////////////////////////////////////////////////////////////// +enum DataRouterHook { + UseScrollRestoration = "useScrollRestoration", + UseSubmitImpl = "useSubmitImpl", + UseFetcher = "useFetcher", +} + +enum DataRouterStateHook { + UseFetchers = "useFetchers", + UseScrollRestoration = "useScrollRestoration", +} + +function getDataRouterConsoleError( + hookName: DataRouterHook | DataRouterStateHook +) { + return `${hookName} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router`; +} + +function useDataRouterContext(hookName: DataRouterHook) { + let ctx = React.useContext(DataRouterContext); + invariant(ctx, getDataRouterConsoleError(hookName)); + return ctx; +} + +function useDataRouterState(hookName: DataRouterStateHook) { + let state = React.useContext(DataRouterStateContext); + invariant(state, getDataRouterConsoleError(hookName)); + return state; +} + /** * Handles the click behavior for router `` components. This is useful if * you need to create custom `` components with the same click behavior we @@ -789,12 +818,7 @@ export function useSubmit(): SubmitFunction { } function useSubmitImpl(fetcherKey?: string, routeId?: string): SubmitFunction { - let dataRouterContext = React.useContext(DataRouterContext); - invariant( - dataRouterContext, - "useSubmitImpl must be used within a Data Router" - ); - let { router } = dataRouterContext; + let { router } = useDataRouterContext(DataRouterHook.UseSubmitImpl); let defaultAction = useFormAction(); return React.useCallback( @@ -909,9 +933,7 @@ export type FetcherWithComponents = Fetcher & { * for any interaction that stays on the same page. */ export function useFetcher(): FetcherWithComponents { - let dataRouterContext = React.useContext(DataRouterContext); - invariant(dataRouterContext, `useFetcher must be used within a Data Router`); - let { router } = dataRouterContext; + let { router } = useDataRouterContext(DataRouterHook.UseFetcher); let route = React.useContext(RouteContext); invariant(route, `useFetcher must be used inside a RouteContext`); @@ -967,8 +989,7 @@ export function useFetcher(): FetcherWithComponents { * routes that need to provide pending/optimistic UI regarding the fetch. */ export function useFetchers(): Fetcher[] { - let state = React.useContext(DataRouterStateContext); - invariant(state, `useFetchers must be used within a DataRouterStateContext`); + let state = useDataRouterState(DataRouterStateHook.UseFetchers); return [...state.fetchers.values()]; } @@ -985,22 +1006,13 @@ function useScrollRestoration({ getKey?: GetScrollRestorationKeyFunction; storageKey?: string; } = {}) { + let { router } = useDataRouterContext(DataRouterHook.UseScrollRestoration); + let { restoreScrollPosition, preventScrollReset } = useDataRouterState( + DataRouterStateHook.UseScrollRestoration + ); let location = useLocation(); let matches = useMatches(); let navigation = useNavigation(); - let dataRouterContext = React.useContext(DataRouterContext); - invariant( - dataRouterContext, - "useScrollRestoration must be used within a DataRouterContext" - ); - let { router } = dataRouterContext; - let state = React.useContext(DataRouterStateContext); - - invariant( - router != null && state != null, - "useScrollRestoration must be used within a DataRouterStateContext" - ); - let { restoreScrollPosition, preventScrollReset } = state; // Trigger manual scroll restoration while we're active React.useEffect(() => { diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index 020e0c63be..0e9234e919 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -663,6 +663,10 @@ export function _renderMatches( } enum DataRouterHook { + UseRevalidator = "useRevalidator", +} + +enum DataRouterStateHook { UseLoaderData = "useLoaderData", UseActionData = "useActionData", UseRouteError = "useRouteError", @@ -672,9 +676,21 @@ enum DataRouterHook { UseRevalidator = "useRevalidator", } -function useDataRouterState(hookName: DataRouterHook) { +function getDataRouterConsoleError( + hookName: DataRouterHook | DataRouterStateHook +) { + return `${hookName} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router`; +} + +function useDataRouterContext(hookName: DataRouterHook) { + let ctx = React.useContext(DataRouterContext); + invariant(ctx, getDataRouterConsoleError(hookName)); + return ctx; +} + +function useDataRouterState(hookName: DataRouterStateHook) { let state = React.useContext(DataRouterStateContext); - invariant(state, `${hookName} must be used within a DataRouterStateContext`); + invariant(state, getDataRouterConsoleError(hookName)); return state; } @@ -683,7 +699,7 @@ function useDataRouterState(hookName: DataRouterHook) { * no navigation is in progress */ export function useNavigation() { - let state = useDataRouterState(DataRouterHook.UseNavigation); + let state = useDataRouterState(DataRouterStateHook.UseNavigation); return state.navigation; } @@ -692,12 +708,8 @@ export function useNavigation() { * as the current state of any manual revalidations */ export function useRevalidator() { - let dataRouterContext = React.useContext(DataRouterContext); - invariant( - dataRouterContext, - `useRevalidator must be used within a DataRouterContext` - ); - let state = useDataRouterState(DataRouterHook.UseRevalidator); + let dataRouterContext = useDataRouterContext(DataRouterHook.UseRevalidator); + let state = useDataRouterState(DataRouterStateHook.UseRevalidator); return { revalidate: dataRouterContext.router.revalidate, state: state.revalidation, @@ -709,7 +721,9 @@ export function useRevalidator() { * parent/child routes or the route "handle" property */ export function useMatches() { - let { matches, loaderData } = useDataRouterState(DataRouterHook.UseMatches); + let { matches, loaderData } = useDataRouterState( + DataRouterStateHook.UseMatches + ); return React.useMemo( () => matches.map((match) => { @@ -733,7 +747,7 @@ export function useMatches() { * Returns the loader data for the nearest ancestor Route loader */ export function useLoaderData(): unknown { - let state = useDataRouterState(DataRouterHook.UseLoaderData); + let state = useDataRouterState(DataRouterStateHook.UseLoaderData); let route = React.useContext(RouteContext); invariant(route, `useLoaderData must be used inside a RouteContext`); @@ -751,7 +765,7 @@ export function useLoaderData(): unknown { * Returns the loaderData for the given routeId */ export function useRouteLoaderData(routeId: string): unknown { - let state = useDataRouterState(DataRouterHook.UseRouteLoaderData); + let state = useDataRouterState(DataRouterStateHook.UseRouteLoaderData); return state.loaderData[routeId]; } @@ -759,7 +773,7 @@ export function useRouteLoaderData(routeId: string): unknown { * Returns the action data for the nearest ancestor Route action */ export function useActionData(): unknown { - let state = useDataRouterState(DataRouterHook.UseActionData); + let state = useDataRouterState(DataRouterStateHook.UseActionData); let route = React.useContext(RouteContext); invariant(route, `useActionData must be used inside a RouteContext`); @@ -774,7 +788,7 @@ export function useActionData(): unknown { */ export function useRouteError(): unknown { let error = React.useContext(RouteErrorContext); - let state = useDataRouterState(DataRouterHook.UseRouteError); + let state = useDataRouterState(DataRouterStateHook.UseRouteError); let route = React.useContext(RouteContext); let thisRoute = route.matches[route.matches.length - 1];