๐บ๏ธ fetcher, submit, and Form Changes #7698
Replies: 20 comments 20 replies
-
Love this RFC. +1 from me. The main pro for me personally is the removal of the need for extra states and effects in your app to track fetcher state and data. Asides from that, I see no issue with this RFC since it doesn't deduct anything from original implementation |
Beta Was this translation helpful? Give feedback.
-
Should note that |
Beta Was this translation helpful? Give feedback.
-
Yep. I'm on board! Thanks for solving this with such a simple solution |
Beta Was this translation helpful? Give feedback.
-
I like it! This'll make a few things I'm doing a lot simpler. |
Beta Was this translation helpful? Give feedback.
-
I was kinda "oh yeah I can see this being useful"... until this
took me over the edge into full blown convert. Yes. Please. |
Beta Was this translation helpful? Give feedback.
-
What would happen for |
Beta Was this translation helpful? Give feedback.
-
Yes please! all my fetchers that are in collapsable menu's will now just work without any workarounds ๐๐พ |
Beta Was this translation helpful? Give feedback.
-
Perfect! This will also solve issue with form in dialog. Now you can unmount dialog instead of hiding it while fetcher is doing it's job. @ryanflorence after this we need promise / lifecycle for fetchers and that's it. I can get rid of ugly useEffects |
Beta Was this translation helpful? Give feedback.
-
Perfect, exactly what I needed, no more workarounds and state hoisting ๐ |
Beta Was this translation helpful? Give feedback.
-
This will enable a nice pattern here a fetcher could be defined globally with a Symbol key: const key = Symbol()
export function useMyFetcher() {
return useFetcher({ persist: key })
} Now // app/routes/api.something.ts
const key = Symbol()
export async function loader() {
return json(...)
}
export function Something() {
let fetcher = useFetcher({ persist: key })
return ...
} And with this you can import Something which already knows how to fetcher the loader and it will always be a single instance. If it needs to be rendered in a list, we could also use Symbols by setting it in a state instead of a UUID. function Something() {
let [key] = useState(() => Symbol());
let fetcher = useFetcher({ persist: key });
return ...
} And now each instance can have a unique key |
Beta Was this translation helpful? Give feedback.
-
In a side project of mine, I have a custom function Form({ handler, ...props }) {
const fetcher = useFetcher();
useEffect(() => {
// call handler optimistically with `fetcher.formData` or `fetcher.data`
}, [handler, fetcher])
return handler ? <Form {...props} /> : <fetcher.Form {...props} /> I take a look at this component from time to time and don't like it a lot, but it provides some kind of symmetry and I can add optimistic stuff by adding a single handler. |
Beta Was this translation helpful? Give feedback.
-
With Form supporting navigate={false} and fetcherKey, why would I need useFetcher? Also how should I access the Form data if it used a fetcher? I imagine I can't use useActionData anymore, same for useNavigation. Will I need to grab it from useFetchers? |
Beta Was this translation helpful? Give feedback.
-
This is being implemented in remix-run/react-router#10949 and #7704 |
Beta Was this translation helpful? Give feedback.
-
I just worked with an experimental version of this and it's :chefs_kiss: There were some use cases that were way too difficult that I'd been wanting to solve and this nails it. |
Beta Was this translation helpful? Give feedback.
-
I like that this would get rid of fetchers and forms with stale data, but I think this is a big enough change that it should be a feature flag. What would be the need for |
Beta Was this translation helpful? Give feedback.
-
This will also enable another thing, reset a fetcher. let [key, setKey] = useState(() => Symbol())
let fetcher = useFetcher({ key })
function resetFetcher() {
setKey(Symbol())
} Now if you call |
Beta Was this translation helpful? Give feedback.
-
This was released in React Router 6.18.0 and Remix 2.2.0 |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
Was Quoting the original RFC: "The fetcher will also have a new prop on I'm trying to use that to create an optimistic mutation with:
|
Beta Was this translation helpful? Give feedback.
-
Is there any way to still manually cancel a request with this? Previously it was possible to just unmount the component that renders the fetcher. A use case might be a large file upload that can be aborted by the user. |
Beta Was this translation helpful? Give feedback.
-
Important
This proposal has changed significantly since originally posted, some comments might not make sense
This proposal has three parts to it:
<Form navigate={false} />
to create a fetcher instead of a navigationsubmit(data, { navigate: false })
to create a fetcher instead of a navigationuseFetcher({ key })
,<Form fetcherKey>
andsubmit(d, { fetcherKey })
Background
Today fetcher cleanup is coupled to the React lifecycle. When a component with a
useFetcher
unmounts and the fetcher is pending, the request is cancelled and the fetcher is removed from internal router state. This decision was made to prevent fetchers from being orphaned and causing memory leaks, and the UI lifecycle seemed like a good place to do it.However, sometimes you want the request to persist beyond the lifecycle of the component, especially with "out of place" optimistic UI and components that open and close like dialogs and menu buttons. The current design makes these kinds of UI more difficult than they should be to build.
Apps need a way to simply toss out a fetcher and not care about the result in-place, but still have access to in
useFetchers
for optimistic UI and even be able to "pick it back up" in some other part of the UI.For example, here is an implementation of Optimistic Add today that we think is overly cumbersome and leaks implementation details of fetcher lifecycles.
First is a list that reads loader data and renders all three things: the form, the pending items, and the items from the database.
The
PendingItem
component only exists to keep the fetcher alive until the request is complete.And finally the
Item
component isn't relevant, we'll just assume that might change what it renders based on being an optimistic version or not.This extra work is only needed because the fetcher lifecycle is coupled to the component lifecycle. There are also some bugs for the observant reader that always creep in when you synchronize state.
Proposal
<Form navigate={bool}>
andsubmit(data, { navigate: bool })
Today
<Form>
always navigates and<fetcher.Form>
creates a fetcher and tracks its state. This proposal adds anavigate
prop to<Form>
that allows it to create a fetcher instead of navigating.Now instead of causing a navigation, an internal fetcher will be created, accessible from
useFetchers
.This allows forms to simply kick off a fetcher and not care about the result, decoupling the fetcher from the lifecycle so that optimistic UIs can render until they are filled in with loader data after revalidation, or conditional UI like dialogs can simply close without cancelling the request and subsequent revalidation.
On the imperative end of the spectrum,
submit
is simply the imperative version of<Form>
. It works the same here: usenavigate: false
and a fetcher will be created instead of a navigation.Fetcher Cleanup changes
If
<Form navigate={false}>
can kick of fetchers that live beyond the lifecycle of the component, we need a new way to clean them up. This proposal changes the internals of fetcher cleanup to decouple it from the React lifecycle.Today
fetcher.data
is managed internally by the router outside of React (which is why the React lifecycle for cleanup made sense).This change will move the "complete" state to React, allowing the router to simply track fetchers while they are pending, giving us a new point of garbage collection decoupled from rendering.
This might be a "breaking bug" (๐ ) for
useFetchers
. In some cases,useFetchers
returns fetchers that are "idle" and havefetcher.data
available on them even though it was never intended to do so. Even the docs explain it differently than the behavior.Most use cases we've seen and authored check some sort of pending information like
fetcher.formData
so we don't expect it to affect very many apps. It might, so we're still considering putting this all behind a future flag, but we'd rather not.Even if it does lead to undesired behavior,
useFetchers
is ephemeral, so it's unlikely your app will "break break", just some ephemeral states you used to have won't be triggered anymore. We think you'll like the amount of code you can delete by moving to the API updates in this proposal.useFetcher({ key })
Lastly, this proposal adds a new
key
API for fetchers that allows you to pick up a fetcher in a different part of the tree from where it was created, or even share it across the tree.Additionally, you can use keys with
Form
andsubmit
:Now an optimistic version of a record can pick up the fetcher that created it, or two distant parts of a tree can share a fetcher.
The fetcher will also have a new prop on
fetcher.key
which can make it easier to filter out the fetchers you want instead of using formData.Examples
Optimistic Add to List
We can revisit the optimistic add code and see how simple it gets:
Form will create new fetchers, they'll be rendered the duration of their request/revalidation cycle, and then will be filled in by loader data when the action is complete.
For some serious React you could create client IDs and use them as the element
key
prop and then they could be fully interactive, as long as the backend understands it.Optimistic Move
Imagine a trello-like interface where cards can be dragged from one column to another. The card that is dragged does not initiate the fetch, but rather the card being dropped does. The
onDrop
even can get the ID of the dragged card and simply kick of a fetch that that id and key.Now the optimistic card in the new column can pick up the fetcher to render pending states or even use it for other operations.
Dialogs and Menus
Dialogs and menus can simply be a form that closes on submission and the fetch won't be cancelled.
Beta Was this translation helpful? Give feedback.
All reactions