The original tkdodo. Eu/blog/effect…

Query Keys are a very important core concept in React Query. They are necessary so that the library can properly cache your data internally and automatically retrieve it when a dependency on the query changes. Finally, it allows you to manually interact with the cache when you need to, such as when updating data after mutation or when you need to manually invalidate some queries.

Before I show you how I personally organize query keys to do these things most efficiently, let’s take a quick look at what these three things mean.

Cache data

Internally, the cache is just a JavaScript object where the keys are the serialized query keys and the values are your query data and meta information. Keys hash them in a deterministic way, so you can also use objects (but at the top level, keys must be strings or arrays).

The most important part is that the key must be unique to your query. If React Query finds the key’s entry in the cache, it will use the cache. Also note that you cannot use the same key for useQuery and useInfiniteQuery. After all, there is only one Query Cache, and you will be sharing data between the two, which is not good because infinite queries have a fundamentally different structure from “normal” queries.

useQuery(['todos'], fetchTodos)

// 🚨 doesn't work
useInfiniteQuery(['todos'], fetchInfiniteTodos)

// ✅ use a different key name
useInfiniteQuery(['infiniteTodos'], fetchInfiniteTodos)
Copy the code

Automatically take

Queries are declarative.

This is a very important concept that cannot be stressed enough, and one that may take some time to sink in. Most people think of queries, particularly retrieval, in an imperative manner.

I have a query that gets some data. Now I click on this button and I want to get it back, but with different parameters. I’ve seen many attempts like this:

// imperative refetch
function Component() {
  const { data, refetch } = useQuery(['todos'], fetchTodos)

  // ❓ how to pass the new parameter to ❓
  return <Filters onApply={()= >refetch(???) } / >
}
Copy the code

The answer is: you didn’t.

This is not what Refetch is for – it is for refetching with the same parameters.

If you have something that changes the state of your data, all you need to do is put it in the Query Key, because React Query will automatically trigger refetch whenever the Key changes. So, when you want to apply filters, just change your client state:

// query key-driven query
function Component() {
  const [filters, setFilters] = React.useState()
  const { data } = useQuery(['todos', filters], () = > fetchTodos(filters))

  // ✅ set local state and let it "drive" the query
  return <Filters onApply={setFilters} />
}
Copy the code

The rerendering triggered by the setFilters update will pass a different Query Key to the React Query, which will refetch it. I have a deeper example of treating the query key as a dependent array in part 1 — the React-Query practice.

Manual interaction

Manual interaction with the query cache is the most important aspect of querying the key structure. Many interactive methods, such as invalidateQueries or setQueriesData support query filters, which allow you to vaguely match your query keys.

A valid React query key

Please note that these views reflect my personal views (in fact, like everything on this blog), so don’t take them as an absolute must when using the query key. I’ve found that these strategies work best when your application becomes more complex, and they scale well.

The collocation

If you haven’t read Kent C. Dodds’ article on maintainability through hosting, do so. I’m not convinced that storing all queryKeys globally in/SRC /utils/ querykeys.ts will make things any better. I place my query keys next to their respective queries in the functional directory, for example:

- src
  - features
    - Profile
      - index.tsx
      - queries.ts
    - Todos
      - index.tsx
      - queries.ts
Copy the code

The Query file will contain everything related to React Query. I usually only export custom hooks, so the actual query functions and query keys are kept locally.

Always use array keys

Yes, the query key can also be a string, but for consistency, I like to always use arrays. React Query converts them internally to Array anyway, so:

// Always use array keys
// 🚨 internally converts to ['todos']
useQuery('todos')
/ / ✅
useQuery(['todos'])
Copy the code

structure

Structure your query keys from the most generic to the most specific, using the level of granularity you see fit. Here’s how I would build a to-do list that allows filterable lists and detailed views:

['todos', 'list', { filters: 'all' }]
['todos', 'list', { filters: 'done' }]
['todos', 'detail', 1]
['todos', 'detail', 2]
Copy the code

Using this structure, I can invalidate all to-do lists, all lists, or all details associated with [‘todos’], and locate a particular list if I know the exact key. Updates from Mutation Responses are therefore more flexible, as you can locate all lists if necessary:

// Updates from the mutation response
function useUpdateTitle() {
  return useMutation(updateTitle, {
    onSuccess: (newTodo) = > {
      // ✅ update todo details
      queryClient.setQueryData(['todos'.'detail', newTodo.id], newTodo)

      // ✅ updates all lists containing this todo
      queryClient.setQueriesData(['todos'.'list'].(previous) = >
        previous.map((todo) = > (todo.id === newTodo.id ? newtodo : todo))
      )
    },
  })
}
Copy the code

This may not work if the structure and details of the list differ greatly, so you can of course invalidate all lists as well:

// Invalidate all lists
function useUpdateTitle() {
  return useMutation(updateTitle, {
    onSuccess: (newTodo) = > {
      queryClient.setQueryData(['todos'.'detail', newTodo.id], newTodo)

      // ✅ invalidates all lists
      queryClient.invalidateQueries(['todos'.'list'])}})}Copy the code

If you know which list you are currently in, for example by reading the filter from the URL, so you can construct the exact query key, you can also combine the two methods and call setQueryData in your list and invalidate all the other methods:

/ / in combination with
function useUpdateTitle() {
  // imagine a custom hook that returns the current filters,
  // stored in the url
  const { filters } = useFilterParams()

  return useMutation(updateTitle, {
    onSuccess: (newTodo) = > {
      queryClient.setQueryData(['todos'.'detail', newTodo.id], newTodo)

      // ✅ Updates the current list
      queryClient.setQueryData(['todos'.'list', { filters }], (previous) = >
        previous.map((todo) = > (todo.id === newTodo.id ? newtodo : todo))
      )

      // 🥳 invalidates the list, but does not re-request active
      queryClient.invalidateQueries({
        queryKey: ['todos'.'list'].refetchActive: false,})},})}Copy the code

Use the query key factory

In the example above, you can see that I manually declared many query keys. This is not only error-prone, but also makes it harder to change in the future, for example, if you find that you want to add another level of granularity to the key.

This is why I recommend one query key factory per function. It’s just a simple object with an entry and a function that generates query keys that you can then use in custom hooks. For the example structure above, it would look like this:

// Query key factory
const todoKeys = {
  all: ['todos'] as const.lists: () = > [...todoKeys.all, 'list'] as const.list: (filters: string) = > [...todoKeys.lists(), { filters }] as const.details: () = > [...todoKeys.all, 'detail'] as const.detail: (id: number) = > [...todoKeys.details(), id] as const,}Copy the code

This gives me a lot of flexibility because each level is built on top of each other, but can still be accessed independently:

// 🕺 delete all related data
queryClient.removeQueries(todoKeys.all)

// 🚀 invalidates lists
queryClient.invalidateQueries(todoKeys.lists())

// 🙌 prefetches a todo detail
queryClient.prefetchQueries(todoKeys.detail(id), () = > fetchTodo(id))
Copy the code