Note: This is the written version of the talk I gave at the Austin Remix October Meetup.
Update 10/02/2024: The Remix team is working on a fix for this! 🎉. When this is released I will update these demos with the new solution.
At the time of writing this, I have been working on Remix applications for the past ~2 years. Hopefully this helps others work through some issues I have seen on many different projects!
So a quick recap on fetchers. The useFetcher() hook is a way to make client API requests to your Remix loaders or actions. The main reason to reach for this hook would be if you need to perform submits with JavaScript and without redirecting the user. Here is an example of that:
Loading Gist...If you are using a traditional form and named form inputs, you can adapt this fetcher to be written with fetcher.form. Typically you would reach for this when you are already using an HTML form or Remix Form and want to remove navigation. This is what that would look like:
Loading Gist...Another thing to note is the useFetcher hook takes an optional options object that can be used to set a key. This key allows you to access the data and loading states globally in your application. This is a great example to show how this would save the effort of lifting state or creating a context to store these values.
Loading Gist...One final thing which will be important for later is that the fetcher implementation contains a useEffect cleanup function that will queue the deletion of the fetcher on unmount source.
Loading Gist...After working with Remix Forms it starts to feel like the useFetcher() hook is a last resort for anti-pattern behavior. Even so, there are just some times where you need to reach for it. Whether you are new to Remix or have been using it for a while, you may have experienced these issues with fetchers:
Juggling the data response, custom error, or successes, and the dependencies can be ugly. This can also feel strange after working with a promise based like Fetch, or Axios or something with callbacks like TanStack Query or Apollo Query.
While searching for answers and guidance on this, I stumbled across some great points on this topic from Tom MacWright in Remix fetcher and action gripes. If I find some more on this topic I will add them here.
This one is a bit more subtle. If you have a fetcher that has not unmounted, it may contain stale data and cause issues. More often than not you may be merely hiding these visually with CSS rather than unmounted the components. Examples of where this can commonly happen are in Dialogs, Drawers, or Popovers to name a few.
If you are using the key option on the useFetcher() hook, you might need to leverage a Symbol or a UUID depending on your needs. Once the key is created, it will be stored globally until the page is fully reloaded. In the shopping cart example from above, the `add-to-bag` key will be stored and accessible throughout the application.
An additional fold of complexity could happen where you need another scope for these keys. Maybe you need a fetcher key for one specific page but for nothing else. Now when you trigger a navigation event and return, this fetcher will still contain the original data.
This might be an easy fix in some scenarios, however it might be easily overlooked when revisiting. With this approach, wrapping the fetcher and conditionally rendering with a visibility state variable would allow for automatic resetting. Something to keep in mind too is that this could add complexity to your animations or transitions.
This is a nice utility to have if you run into a situation where you need to manually control the fetcher data. Most people naturally reach for this approach in their component, so isolating this state in a wrapper hook is a little more elegant.
The issues I have with this are:
There is a few ways to handle this. You could use a Symbol, a UUID, or a random number. This would allow you to create a new key every time the fetcher is called. You can also add a reset that modifies that key.
One additional use case to consider is if you need to access the fetcher elsewhere, the key needs to be shared via the loader or a context provider. I would probably recommend using the loader to generate the key and taking advantage of the useRouteLoader() hook to handle the context so you don't have to. However this wouldn't work across multiple pages.
A few downsides are:
Hopefully a mix of these three solutions can help with your Remix project. I would recommend using the custom fetcher wrapper as a first step, and then moving to random fetcher keys if you need to. If you are still having issues, I would recommend reaching out to the Remix team or the community for help.
Additionally, here are some other resources add as a reference: