apollo-client
apollo-client copied to clipboard
useLazyQuery is called on every re-render if we change the query variables
I'm not sure if it’s a bug or not. In case if it's not, we have to update the doc, saying that you still will execute the query if you update your query variables.
The original issue. It affects the latest apollo client version.
I've re-read the doc, I think this is a bug. The whole point of useLazyQuery is executing queries in response to events other than component rendering, but this is what exactly happens when we change the query variables - we make a network call every single render (in case of only-network or cache-and-network policies).
Thoughts @benjamn? Not sure what to do with it.
If you pass the variables when you use the function to call the query instead of setting them on the query itself this won't happen.
It doesn't make sense for updating the variables inside the query to do this. Most people would expect either method to only execute the query when the function is called.
demonstration of the query executing on every render (variables set on instantiation): https://codesandbox.io/s/vigorous-forest-8609l
demonstration of the query only executing on function call (variables set on call): https://codesandbox.io/s/bold-platform-jk2wo
Thanks for the alternative solution @Relsig. @alexpermyakov I'm new to Apollo, so naturally I thought I missed something...came across this after trying to debug for too long. Nice to know I'm not the only one!
@pmcorrea
I also spend lot of time time on this issue, I am new to apollo. Either we need to add this to doc or need to make a fix. I will check if I can do something on this.
@Relsig thanks for sharing a quick solution.
As per this line, the query will be executed again if any of the options
passed to the useLazyQuery
hook changes. This behaviour is same for useLazyQuery
and useQuery
.
I think we can do any one of the below to close this issue:
- Remove the re execution of the query when the
options
passed changes in the case ofuseLazyQuery
. - Add a new sentence in the doc something like below
The query will be executed again if any of the
options
passed to theuseLazyQuery
hook changes. This behaviour is same foruseLazyQuery
anduseQuery
.
The problem with this 1st solution is that it might break somebody's code. So I think it is better to add the sentence to the doc.
I will raise a pr with the change.
Since the whole point of useLazyQuery
is to have better control over when exactly the query runs, I think this behavior should be changed to better match user expectations, personally.
Also ran into this problem today and was confused about the naming, seems like useLazyQuery
turns into regular useQuery
after its been ran once.
Is anyone working around this in some clever ways?
@tholman if you want to have better control over when queries fire, it's probably easier to use the apollo client directly:
const client = useApolloClient();
const response = await client.query({
query: SOME_QUERY,
variables: ...
fetchPolicy: 'no-cache'
});
That API is more predictable than useQuery
and useLazyQuery
.
I've tried keeping everything stable using useMemo
but useLazyQuery
just kept firing on each render the moment I added any type of fetchPolicy
. When removing fetchPolicy
it does work the way I wanted it to (but not an option in my use case).
Turns out the reason for our redundant rerequests were different, it had nothing to do with fetchPolicy
.
In our use case we switch Apollo clients depending on a context. During that switch all useQuery
and useLazyQuery
retrigger even if all variables/options remain the same. Solutions to this are client.query(...)
or skip: true
(for useQuery
).
I have been fighting with this for a few days and just landed here. My useLazyQuery does in fact not re-render now without the fetchPolicy in place. So that's helpful - sort of.
I am definitely losing some efficiency here since the useLazyQuery hook has all the data, loading, etc. built-in so I would like to use that if possible. I would agree that useLazyQuery should definitely not run on a re-render, since that leads to a number of unintended side effects - for example if I clear the string for a variable being sent in with useLazyQuery that was previously set, it re-runs the query with the variable that was present before it was cleared.
Am i losing any additional functionality if I use the API directly instead?
I can't speak to issues others are having, and haven't dug into the code myself to see where a potential bug is / could be, but I wanted to speak to our experience and what was a few days of head banging that was saved by a comment here https://github.com/apollographql/apollo-client/issues/7484#issuecomment-918257833
We had recently refactored our Apollo layer to put createHttpLink()
s and new ApolloClient()
creation inside of a React component. We have a ton of navigation in our app (from react router) and so many times navigating to a new sub-page is what drove our logic to fire off a query returned by useLazyQuery()
, and for better or for worse, our Provider component that instantiated our client was below our <Router>
from react-router.
Because our call to create an ApolloClient wasn't memozied, every route change led to a re-render of our Provider which led to instantiating a new ApolloClient every time. One simple useMemo()
around creating our ApolloClient and all of our headaches went away, useLazyQuery
proceeded to work as is. Hope this can be of use for anyone else that was going down the wrong path on this issue
I can't speak to issues others are having, and haven't dug into the code myself to see where a potential bug is / could be, but I wanted to speak to our experience and what was a few days of head banging that was saved by a comment here #7484 (comment)
We had recently refactored our Apollo layer to put
createHttpLink()
s andnew ApolloClient()
creation inside of a React component. We have a ton of navigation in our app (from react router) and so many times navigating to a new sub-page is what drove our logic to fire off a query returned byuseLazyQuery()
, and for better or for worse, our Provider component that instantiated our client was below our<Router>
from react-router.Because our call to create an ApolloClient wasn't memozied, every route change led to a re-render of our Provider which led to instantiating a new ApolloClient every time. One simple
useMemo()
around creating our ApolloClient and all of our headaches went away,useLazyQuery
proceeded to work as is. Hope this can be of use for anyone else that was going down the wrong path on this issue
Would you mind providing an example? I am doing something similar/exactly the same and I'd like to know how you tackled this. Thanks!
This is still very much a persistent issue on August 24th, 2022.
I also had the same problem. I made the query call only when I click the 'Search' button in the search bar. However, after the button is pressed, the api is called dozens of times whenever input is entered into the input window. (The api is called whenever a variable in variables changes)
import { useLazyQuery } from "@apollo/client";
. . . . . .
so, I changed.
I found the same question in v3.7.2 may be use the following code can get relief.
const [excuteQuery] = useLazyQuery(SomeDocument, {
fetchPolicy: 'network-only',
nextFetchPolicy: 'no-cache',
});
Its still an issue on ^3.8.7
passing options / variables to useLazyQuery
causes to execute the query 😭
This is still an issue after 3 years? What am I missing here. Running into identical issue.
Same issue here.
Same but I'm changing query method, variables stay constant and useLazyQuery
calls a query every time anyway.
This still happens for me even when passing variables in at call time.
I have a hook that I use to export lazyQueries with a custom abort:
export function useAboritiveLazyQuery({
query,
}: {
query: DocumentNode;
}): AbortiveLazyQueryHookOptions {
const abortController = useRef(new AbortController());
const [fetch, queryResult] = useLazyQuery(query, {
context: {
fetchOptions: {
signal: abortController.current.signal,
},
},
fetchPolicy: 'network-only',
nextFetchPolicy: 'no-cache',
});
const cancel = () => {
abortController.current.abort();
abortController.current = new AbortController();
/**
* setting the ref to a new abortController allows the signal
* to be reset for the next time. Considering you are doing
* this on component unmount, you shouldn't need to do it,
* but if you wanted to cancel requests multiple times without
* un-mounting this would be necessary.
* Some workflows may require an abort on unmount. If that is the case, you should use this. (example chat -> search)
*/
};
// if the error is signal is aborted without reason, refetch
// the signal aborted error is a red herring since we are explicitly aborting the request
// apollo client doesn't like this and throws an error. We can safely ignore this error
const error = useMemo(() => {
if (queryResult?.error?.message === 'signal is aborted without reason') {
queryResult.refetch();
return null;
}
return queryResult?.error;
}, [queryResult?.error]);
queryResult.error = error;
return [fetch, { ...queryResult, cancel }];
}
I'm calling fetch in a child component of the component I'm importing this hook into.
So index.tsx is importing the above hook and I'm passing down the fetch to a child component. The child component calls fetch and index.tsx then re-renders. This shouldn't happen. fetch is just making a network call, I removed any effects that resolve that call and update state.
Even when using lazyQuery like this in index.tsx (without the abortive hook):
const [fetchResponse, { data, error }] = useLazyQuery(GET_CHAT_SEARCH);
and passing that fetchResponse down to the child, the re-render still happens. Again, I'm calling with variables at the time of the call, not time of init like the first couple of comments suggest.
@PaulSender are you talking about a network request on rerender or about a rerender on network request here?
This issue is about a network request firing every time variables
changes.
That is actually intended, as in the documentation of the execute
function, it states:
Function that can be triggered to execute the suspended query. After being called,
useLazyQuery
behaves just likeuseQuery
.
Once useLazyQuery
starts to behave like useQuery
, a change to variables
will trigger a new network request, as that would be the case for useQuery
, too.
We acknowledge that this feature is very confusing (especially taking the merging between passed options into account) and are considering to deprecate passing variables
, or even options
, into useLazyQuery
, and to encourage only passing them to execute
in a future version.
Now, @PaulSender, what you describe here sounds like the opposite: If I understand you correctly, when execute
is called, the component is rerendered. That is unrealated to this issue - and expected:
Calling execute
changes the internal state of the hook (e.g. the loading state changes), and as a consequence the component that calls the hook rerenders.
That's how React works, we have very little control over that. But if you want to start minimizing component rerenders, I want to suggest that you take a look at the React Compiler that's currently being open sourced by the React team.
If I misunderstood you in any way, please correct me 🙂
@phryneas you are right. I misunderstood the issue in this thread. Thank you for clarifying