Skip to content

Commit

Permalink
Add useFetcher(key) and <Form navigate={false}> (#10960)
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 committed Oct 26, 2023
1 parent 805924d commit c0dbcd2
Show file tree
Hide file tree
Showing 11 changed files with 560 additions and 129 deletions.
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

0 comments on commit c0dbcd2

Please sign in to comment.