Skip to content

Instantly share code, notes, and snippets.

@butaud
Last active November 3, 2022 03:57
Show Gist options
  • Save butaud/507bcf5c39e5b3605eee2f77da26b3f1 to your computer and use it in GitHub Desktop.
Save butaud/507bcf5c39e5b3605eee2f77da26b3f1 to your computer and use it in GitHub Desktop.
useQuery semantics

useQuery semantics

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.

useQuery is a description, not an operation

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.

What does fetchPolicy do?

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.

What if the underlying data changes after it has been fetched?

Apollo supports several mechanisms to get your component fresher data. Here are some of the more common ones:

Manual refetch

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.

Automatic refetch

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.

Apollo cache push

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.

Manual state override

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment