-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
useMutation returns a new object on every call #1858
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
Comments
The object returned from |
I think it’s expected. useQuery behaves the same now: the functions / objects on the returned object are referentially stable if you need to pass them to memoized components or add as dependency to an effect, but the actual object is not. |
relatively new to using this library, is there a reason for this behaviour? that the returned object is not stable but the values inside the object are? |
It was introduced to make the library concurrent-mode safe. See here: #1775
it’s a minimal tradeoff :) |
makes sense, thanks! |
@TkDodo Thanks a lot for sharing this info. Maybe this is common knowledge among React devs, I certainly didn't expect this. Could you maybe add something to the docs regarding referential stability so it's clear what can be used in deps arrays e.g. for useEffect. |
Sure, I’d accept a PR to the docs :) generally, I’d say it’s good practice to keep your dependencies as narrow as possible, but I agree that we should alert users to this trade off. |
This has been a real gotcha, calling some API hundreds of times per second is not fun. Here is a minimal example of such busted behavior: const SomeComponent = () => {
const { isLoading, data } = useQuery('toggle-state', fetchToggleStateFromApi)
const mutation = useMutation(changeToggleStateInApi)
const handleChange = useCallback(
(state) => {
mutation.mutate(state)
},
// [mutation, row] // DO NOT DO THIS even if eslint recommends you to add "mutation" there
// eslint-disable-next-line
[mutation.mutate]
)
return (<Toggle value={data} disabled={isLoading} onChange={handleChange}>)
} |
@alesmenzel destructuring can help here:
taking this one step further,
which in turn, is just that being said, the I guess the only reason to stick |
I'm running into a similar problem with eslint complaining about exhaustive deps. See facebook/react#21624. Does |
can you show a codesandbox example with the failing lint rule please? |
@TkDodo Here is a very small repro with both the |
@alesmenzel I think this is a bug in the lint rule, which has been fixed already. I updated the sandbox to Not sure which version of the eslint-plugin is used under the hood, or which released fixed it :) |
okay weird, maybe it was just slow - the error is still there. sorry for the confusion. Seems like the only way to get around it is destructuring? I think it is safe to destruct |
Destruction does work but becomes verbose when you have multiple mutations in a single block and using multiple functions in other hooks. To avoid rerunning my dependent hooks every frame I am wrapping the results in a forked version of https://github.com/kentcdodds/use-deep-compare-effect/blob/main/src/index.ts#L28-L43 But that is not enough to guard calls to
|
I still don't understand why it wasn't possible to guarantee the referential integrity of the mutation object, could you elaborate more on this? Just asking because in my point of view this would be a nice thing to have implemented in the lib itself. As @edzis pointed out, destructuring works but it's not a good option for certain cases. |
An alternative to destructuring is using Function.prototype.call (or bind or apply). This explicitly removes the function's dependency on e.g., const myMutation = useMutation(myAsyncFunction);
useEffect(() => {
myMutation.mutateAsync.call(undefined, "first argument");
}, [myMutation.mutateAsync]); |
@TkDodo Can we do something like below?
It seems to work as expected, since you mentioned:
But I don't know if that's a good thing to do or not. |
I run into this issue myself and I'm here to summarize my findings, so maybe they can be useful for others. Below I describe two solution. I prefer the 2nd solution where it's possible. The issue// DEFINITION
import axios from 'axios';
import { useMutation } from '@tanstack/react-query';
const client = axios.create({ baseURL: PATH_BASE });
export const search = async (request: SearchRequest) => (await client.post<SearchResult[]>('/api/search', request)).data;
export const useSearch = () => useMutation({ mutationFn: search });
// USAGE
export const SearchPage = () => {
const searchResult = useSearch();
// ⚠️ Here ESLint adds searchResult into dependencies, so this useEffect(...) runs indefinite amount of times
useEffect(() => searchResult.mutate({}), [searchResult]);
return <>
<SearchBar onChange={searchResult.mutate} />
<SearchTable data={searchResult.data} />
</>; Solution 1: Put
|
Describe the bug
If I'm reading the code right, I think the
useMutation
function creates a new object on every call - so if this object is used directly in the dependency array for any further React hooks, it will trigger additional renders.To Reproduce
Expected behavior
The log line should never fire: the arguments to
useMutation
never change so I would expect the same reference back each time (I think this is similar to howuseQuery
works - at least, I don't seem to have the same issue there).Additional context
Looking at the current code would it be sufficient to memoise the returned object on
currentResult
andmutate
? Or would that likely cause further problems?The text was updated successfully, but these errors were encountered: