Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: add better docs and console errors for data router features #9311

Merged
merged 4 commits into from Sep 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/nasty-bugs-promise.md
@@ -0,0 +1,6 @@
---
"react-router": patch
"react-router-dom": patch
---

docs: Enhance console error messages for invalid usage of data router hooks
3 changes: 3 additions & 0 deletions docs/components/form.md
Expand Up @@ -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).

<docs-warning>This feature only works if using a data router, see [Picking a Router][pickingarouter]</docs-warning>

```tsx
import { Form } from "react-router-dom";

Expand Down Expand Up @@ -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
3 changes: 3 additions & 0 deletions docs/components/scroll-restoration.md
Expand Up @@ -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.

<docs-warning>This feature only works if using a data router, see [Picking a Router][pickingarouter]</docs-warning>

You should only render one of these and it's recommended you render it in the root route of your app:

```tsx [1,7]
Expand Down Expand Up @@ -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
7 changes: 6 additions & 1 deletion docs/hooks/use-fetcher.md
Expand Up @@ -7,10 +7,12 @@ new: true

In HTML/HTTP, data mutations and loads are modeled with navigation: `<a href>` and `<form action>`. Both cause a navigation in the browser. The React Router equivalents are [`<Link>`][link] and [`<Form>`][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.

<docs-warning>This feature only works if using a data router, see [Picking a Router][pickingarouter]</docs-warning>

This is useful when you need to:

- fetch data not associated with UI routes (popovers, dynamic forms, etc.)
Expand Down Expand Up @@ -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
3 changes: 3 additions & 0 deletions docs/hooks/use-fetchers.md
Expand Up @@ -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.)

<docs-warning>This feature only works if using a data router, see [Picking a Router][pickingarouter]</docs-warning>

```tsx
import { useFetchers } from "react-router-dom";

Expand Down Expand Up @@ -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
4 changes: 3 additions & 1 deletion docs/hooks/use-matches.md
Expand Up @@ -39,7 +39,7 @@ A `match` has the following shape:

Pairing `<Route handle>` with `useMatches` gets very powerful since you can put whatever you want on a route `handle` and have access to `useMatches` anywhere.

<docs-warning>`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.</docs-warning>
<docs-warning>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.</docs-warning>

## Breadcrumbs

Expand Down Expand Up @@ -98,3 +98,5 @@ function Breadcrumbs() {
```

Now you can render `<Breadcrumbs/>` anywhere you want, probably in the root component.

[pickingarouter]: ../routers/picking-a-router
3 changes: 3 additions & 0 deletions docs/hooks/use-navigation.md
Expand Up @@ -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

<docs-warning>This feature only works if using a data router, see [Picking a Router][pickingarouter]</docs-warning>

```js
import { useNavigation } from "react-router-dom";

Expand Down Expand Up @@ -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
3 changes: 3 additions & 0 deletions docs/hooks/use-revalidator.md
Expand Up @@ -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.

<docs-warning>This feature only works if using a data router, see [Picking a Router][pickingarouter]</docs-warning>

```tsx
import { useRevalidator } from "react-router-dom";

Expand Down Expand Up @@ -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
3 changes: 3 additions & 0 deletions docs/hooks/use-route-error.md
Expand Up @@ -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.

<docs-warning>This feature only works if using a data router, see [Picking a Router][pickingarouter]</docs-warning>

```jsx
function ErrorBoundary() {
const error = useRouteError();
Expand All @@ -33,3 +35,4 @@ function ErrorBoundary() {

[errorelement]: ../route/error-element
[isrouteerrorresponse]: ../utils/is-route-error-response
[pickingarouter]: ../routers/picking-a-router
2 changes: 2 additions & 0 deletions docs/hooks/use-route-loader-data.md
Expand Up @@ -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.

<docs-warning>This feature only works if using a data router, see [Picking a Router][pickingarouter]</docs-warning>

```tsx
import { useRouteLoaderData } from "react-router-dom";

Expand Down
8 changes: 7 additions & 1 deletion docs/hooks/use-submit.md
Expand Up @@ -5,7 +5,11 @@ new: true

# `useSubmit`

The imperative version of `<Form>` 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 `<Form>` that let's you, the programmer, submit a form instead of the user.

<docs-warning>This feature only works if using a data router, see [Picking a Router][pickingarouter]</docs-warning>

For example, submitting the form every time a value changes inside the form:

```tsx [8]
import { useSubmit, Form } from "react-router-dom";
Expand Down Expand Up @@ -87,3 +91,5 @@ submit(null, {
// same as
<Form action="/logout" method="post" />;
```

[pickingarouter]: ../routers/picking-a-router
3 changes: 2 additions & 1 deletion docs/route/action.md
Expand Up @@ -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.

<docs-error>This feature only works if using a data router</docs-error>
<docs-warning>This feature only works if using a data router, see [Picking a Router][pickingarouter]</docs-warning>

```tsx
<Route
Expand Down Expand Up @@ -130,6 +130,7 @@ You can `throw` in your action to break out of the current call stack (stop runn
For more details and expanded use cases, read the [errorElement][errorelement] documentation.

[loader]: ./loader
[pickingarouter]: ../routers/picking-a-router
[dynamicsegments]: ./route#dynamic-segments
[formdata]: https://developer.mozilla.org/en-US/docs/Web/API/FormData
[request]: https://developer.mozilla.org/en-US/docs/Web/API/Request
Expand Down
3 changes: 2 additions & 1 deletion docs/route/error-element.md
Expand Up @@ -7,7 +7,7 @@ new: true

When exceptions are thrown in [loaders][loader], [actions][action], or component rendering, instead of the normal render path for your Routes (`<Route element>`), the error path will be rendered (`<Route errorElement>`) and the error made available with [`useRouteError`][userouteerror].

<docs-error>This feature only works if using a data router</docs-error>
<docs-warning>This feature only works if using a data router, see [Picking a Router][pickingarouter]</docs-warning>

```tsx
<Route
Expand Down Expand Up @@ -210,6 +210,7 @@ The project route doesn't have to think about errors at all. Between the loader
[loader]: ./loader
[action]: ./action
[userouteerror]: ../hooks/use-route-error
[pickingarouter]: ../routers/picking-a-router
[response]: https://developer.mozilla.org/en-US/docs/Web/API/Response
[isrouteerrorresponse]: ../utils/is-route-error-response
[json]: ../fetch/json
3 changes: 3 additions & 0 deletions docs/route/should-revalidate.md
Expand Up @@ -7,6 +7,8 @@ new: true

This function allows you opt-out of revalidation for a route's loader as an optimization.

<docs-warning>This feature only works if using a data router, see [Picking a Router][pickingarouter]</docs-warning>

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>`][form].
Expand Down Expand Up @@ -76,3 +78,4 @@ interface ShouldRevalidateFunction {
[loader]: ./loader
[useloaderdata]: ../hooks/use-loader-data
[params]: ./route#dynamic-segments
[pickingarouter]: ../routers/picking-a-router
60 changes: 36 additions & 24 deletions packages/react-router-dom/index.tsx
Expand Up @@ -642,6 +642,35 @@ if (__DEV__) {
//#region Hooks
////////////////////////////////////////////////////////////////////////////////

enum DataRouterHook {
UseScrollRestoration = "useScrollRestoration",
UseSubmitImpl = "useSubmitImpl",
UseFetcher = "useFetcher",
}

enum DataRouterStateHook {
UseFetchers = "useFetchers",
UseScrollRestoration = "useScrollRestoration",
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enums to represent the hooks that require a data router


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;
}
Comment on lines +662 to +672
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All hooks should use the internal hooks to get access to the contexts, so that we get consistent error messaging across the board


/**
* Handles the click behavior for router `<Link>` components. This is useful if
* you need to create custom `<Link>` components with the same click behavior we
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -909,9 +933,7 @@ export type FetcherWithComponents<TData> = Fetcher<TData> & {
* for any interaction that stays on the same page.
*/
export function useFetcher<TData = any>(): FetcherWithComponents<TData> {
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`);
Expand Down Expand Up @@ -967,8 +989,7 @@ export function useFetcher<TData = any>(): FetcherWithComponents<TData> {
* 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()];
}

Expand All @@ -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(() => {
Expand Down