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

Add useFetcher(key) and <Form navigate={false}> #10960

Merged
merged 4 commits into from
Oct 26, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/fetcher-key.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"react-router-dom": minor
---

Add support for manual fetcher key specification via `useFetcher({ key: string })` so you can access the same fetcher instance from different components in your application without prop-drilling ([RFC](https://github.com/remix-run/remix/discussions/7698))

- Fetcher keys are now also exposed on the fetchers returned from `useFetchers` so that they can be looked up by `key`
5 changes: 5 additions & 0 deletions .changeset/fix-get-delete-fetcher-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/router": patch
---

Fix `router.getFetcher`/`router.deleteFetcher` type definitions which incorrectly specified `key` as an optional parameter
8 changes: 8 additions & 0 deletions .changeset/form-navigate-false.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"react-router-dom": minor
---

Add `navigate`/`fetcherKey` params/props to `useSumbit`/`Form` to support kicking off a fetcher submission under the hood with an optionally user-specified `key`

- Invoking a fetcher in this way is ephemeral and stateless
- If you need to access the state of one of these fetchers, you will need to leverage `useFetcher({ key })` to look it up elsewhere
9 changes: 9 additions & 0 deletions docs/components/form.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,14 @@ function Project() {

As you can see, both forms submit to the same route but you can use the `request.method` to branch on what you intend to do. After the actions completes, the `loader` will be revalidated and the UI will automatically synchronize with the new data.

## `navigate`

You can tell the form to skip the navigation and use a [fetcher][usefetcher] internally by specifying `<Form navigate={false}>`. This is essentially a shorthand for `useFetcher()` + `<fetcher.Form>` where you don't care about the resulting data and only want to kick off a submission and access the pending state via [`useFetchers()`][usefetchers].

## `fetcherKey`

When using a non-navigating `Form`, you may also optionally specify your own fetcher key to use via `<Form navigate={false} fetcherKey="my-key">`.

## `replace`

Instructs the form to replace the current entry in the history stack, instead of pushing the new entry.
Expand Down Expand Up @@ -367,6 +375,7 @@ You can access those values from the `request.url`
[useactiondata]: ../hooks/use-action-data
[formdata]: https://developer.mozilla.org/en-US/docs/Web/API/FormData
[usefetcher]: ../hooks/use-fetcher
[usefetchers]: ../hooks/use-fetchers
[htmlform]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form
[htmlformaction]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-action
[htmlform-method]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-method
Expand Down
58 changes: 46 additions & 12 deletions docs/hooks/use-fetcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,37 @@ Fetchers have a lot of built-in behavior:
- Handles uncaught errors by rendering the nearest `errorElement` (just like a normal navigation from `<Link>` or `<Form>`)
- Will redirect the app if your action/loader being called returns a redirect (just like a normal navigation from `<Link>` or `<Form>`)

## `fetcher.state`
## Options

You can know the state of the fetcher with `fetcher.state`. It will be one of:
### `key`

- **idle** - nothing is being fetched.
- **submitting** - A route action is being called due to a fetcher submission using POST, PUT, PATCH, or DELETE
- **loading** - The fetcher is calling a loader (from a `fetcher.load`) or is being revalidated after a separate submission or `useRevalidator` call
By default, `useFetcher` generate a unique fetcher scoped to that component (however, it may be looked up in [`useFetchers()`][use_fetchers] while in-flight). If you want to identify a fetcher with your own key such that you can access it from elsewhere in your app, you can do that with the `key` option:

```tsx
function AddToBagButton() {
const fetcher = useFetcher({ key: "add-to-bag" });
return <fetcher.Form method="post">...</fetcher.Form>;
}

## `fetcher.Form`
// Then, up in the header...
function CartCount({ count }) {
const fetcher = useFetcher({ key: "add-to-bag" });
const inFlightCount = Number(
fetcher.formData?.get("quantity") || 0
);
const optimisticCount = count + inFlightCount;
return (
<>
<BagIcon />
<span>{optimisticCount}</span>
</>
);
}
```

## Components

### `fetcher.Form`

Just like `<Form>` except it doesn't cause a navigation. <small>(You'll get over the dot in JSX ... we hope!)</small>

Expand All @@ -83,6 +105,8 @@ function SomeComponent() {
}
```

## Methods

## `fetcher.load()`

Loads data from a route loader.
Expand Down Expand Up @@ -140,7 +164,17 @@ If you want to submit to an index route, use the [`?index` param][indexsearchpar

If you find yourself calling this function inside of click handlers, you can probably simplify your code by using `<fetcher.Form>` instead.

## `fetcher.data`
## Properties

### `fetcher.state`

You can know the state of the fetcher with `fetcher.state`. It will be one of:

- **idle** - nothing is being fetched.
- **submitting** - A route action is being called due to a fetcher submission using POST, PUT, PATCH, or DELETE
- **loading** - The fetcher is calling a loader (from a `fetcher.load`) or is being revalidated after a separate submission or `useRevalidator` call

### `fetcher.data`

The returned data from the loader or action is stored here. Once the data is set, it persists on the fetcher even through reloads and resubmissions.

Expand Down Expand Up @@ -171,7 +205,7 @@ function ProductDetails({ product }) {
}
```

## `fetcher.formData`
### `fetcher.formData`

When using `<fetcher.Form>` or `fetcher.submit()`, the form data is available to build optimistic UI.

Expand Down Expand Up @@ -204,15 +238,15 @@ function TaskCheckbox({ task }) {
}
```

## `fetcher.json`
### `fetcher.json`

When using `fetcher.submit(data, { formEncType: "application/json" })`, the submitted JSON is available via `fetcher.json`.

## `fetcher.text`
### `fetcher.text`

When using `fetcher.submit(data, { formEncType: "text/plain" })`, the submitted text is available via `fetcher.text`.

## `fetcher.formAction`
### `fetcher.formAction`

Tells you the action url the form is being submitted to.

Expand All @@ -223,7 +257,7 @@ Tells you the action url the form is being submitted to.
fetcher.formAction; // "mark-as-read"
```

## `fetcher.formMethod`
### `fetcher.formMethod`

Tells you the method of the form being submitted: get, post, put, patch, or delete.

Expand Down
10 changes: 9 additions & 1 deletion docs/hooks/use-submit.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,15 @@ submit(null, {
<Form action="/logout" method="post" />;
```

Because submissions are navigations, the options may also contain the other navigation related props from [`<Form>`][form] such as `replace`, `state`, `preventScrollReset`, `relative`, `unstable_viewTransition` etc.
Because submissions are navigations, the options may also contain the other navigation related props from [`<Form>`][form] such as:

- `fetcherKey`
- `navigate`
- `preventScrollReset`
- `relative`
- `replace`
- `state`
- `unstable_viewTransition`

[pickingarouter]: ../routers/picking-a-router
[form]: ../components/form
Loading