On my team, we've had some confusion lately about the behavior of useQuery
and how the fetchPolicy
parameter influences it. Looking back through the Apollo documentation, I think it just doesn't do a very good job of explaining some of these subtleties, but the behavior itself is logical once you have the right mental model of what the hook is supposed to do.
Some of the confusion around useQuery's behavior comes from thinking of it as an operation, something that you call to execute a specific query. But this isn't the right way to think about it - instead, you should think of it as a way to tell Apollo that a particular instance of a component is associated with a particular query. Apollo will, of course, fetch the query data and provide the results to the component, but the timing and frequency of the actual fetch depends on the lifecycle of the component, not on the number of times useQuery is called during render passes.
If this seems strange, compare it to a more familiar React hook: useState
. When your code says const [counter, setCounter] = useState(0);
, you are not telling React to set the counter
state to 0. Instead you are telling React that your component has a bit of state with an initial value of 0. It's up to React to set the initial value once and then maintain a stable value it returns between renders. useQuery
works the same way - you describe to Apollo where you want the data to come from and what you want it to look like, and then Apollo's job is to obtain the data and return a stable version of it between renders.
Under the hood, this stable version of the data is stored in the React state for your component instance.
The fetchPolicy
parameter is for describing to Apollo the acceptable conditions for the data it fetches - whether the query data should come from the cache, the network, etc. It is the policy that fetches need to follow, not the policy of when to fetch. Once the data is fetched, the fetchPolicy has been fulfilled! At that point Apollo does its job of returning a stable version of the data that it has fetched no matter how many times your component re-renders and calls useQuery
.
Apollo supports several mechanisms to get your component fresher data. Here are some of the more common ones:
useQuery
returns a helper function called refetch
which can be used to trigger a new fetch at any time. When it is called, it essentially resets the useQuery
linkage to its initial state, so Apollo will do the fetch all over again, following whatever conditions were set by your initial fetchPolicy
.
The query will also be automatically refetched if any of the variables passed in to useQuery
change. The automatic refetch does the exact same thing as a manual refetch.
If the Apollo cache for any of the objects in your query data gets updated, that change gets pushed to the version of the data that is in your component's state, causing your component to re-render with the updated data. This applies unless your query used the no-cache
fetchPolicy. Typically this occurs when a subscription or mutation updates the data your query has fetched.
One thing that's important to note is that if you are using versions of Apollo client >= 3.0 and < 3.6, a cache update for the data that your query is watching will cause your query to refetch its data, even though it probably already has the freshest data due to the cache update. Since this is almost never what you want, if you are using these versions of Apollo client, you should probably set nextFetchPolicy
to "cache-first" if your fetchPolicy
is "network-only" or "cache-and-network". For these versions, nextFetchPolicy
governs what happens when the cache for your query is updated.
The stable version of the data that's stored in the component state can also be edited using the updateQuery
helper function which is returned by useQuery
, triggering a re-render with the updated data. This can be used for a variety of cases, such as updating the data with the results of a subscription event or optimistically applying mutation changes before they've been routed through the backend.
This option is usually only needed if the Apollo cache push can't update the query automatically. Common cases where it is not needed:
- Subscription updates to an existing object - these automatically update the cache and trigger a rerender
- Mutations that update an existing object - as long as the mutation follows the recommended pattern of returning the updated object, the cache will automatically be updated and trigger a rerender
A typical case where this might be required is when a query's top level data is a list and a new object is added or removed from the list.