Query Key Factory - React Query 的 QueryKey 查询管理工具

858 阅读7分钟

使用Query Key Factory的契机

在使用react-query做了几个项目以后,就感觉react-query中的查询和请求状态管理非常方便。但逐渐发现对react-query的使用需求在逐步发生变化:

  1. 基本需求就是对异步数据获取的状态进行管理。
  2. 获取到数据,react-query对数据进行缓存后,需要对缓存数据进行管理。
  3. 当需要进行数据交互通过useMutation获取到新的数据时,就需要通过之前查询数据的useQueryqueryKey修改缓存数据,通过query client中的setQueryData来修改缓存,或者使用invalidateQueries来使对应查询的queryKey让缓存失效来重新获取数据,最终就变成对queryKey的管理。

最近在做的real world练手项目中就碰到很多这种交互产生的问题,项目类似于Medium的一个多人博客,可以查看、发布文章、收藏文章、关注博主、对文章发布评论等,使用react-query来做API请求是非常简单的,但是在列表中点击收藏文章按钮,来收藏文章,在文章详情点击收藏按钮来收藏文章、关注博主。在文章详情中,对文章进行评论,这样就会产生大量的交互操作,需要在发起新的请求后修改之前的查询数据缓存或使之前的缓存失效,让新的数据生效。

这样我在第一个版本中,使用了一个valtiostore对当前请求的queryKey进行管理,如果在加载到当前页面有请求发生,则会通过setKey将当前请求的queryKey放到store中,如果当前页面有交互发生,需要使用invalidateQueries来使当前页面的缓存失效,就可以在useMutation时,通过getKey来获取当前请求的queryKey,从而来更新数据。

这样能在一定层面上解决queryKey的问题,但也存在几个问题:

  1. 写法上就很繁琐,每次useMutation都需要先从store中获取下key
  2. store中获取的key也是有局限性的。无法使用多个invalidateQueries来让多个位置的请求缓存失效,仅限当前页面。如果是在列表1中收藏的文章,列表2中的相同文章的收藏按钮不一定更新了这个状态,跟react-query的缓存时效和机制有关。

基于上面的问题到react-query的官方找了下,看下又没有类似的解决办法,果然官方有推荐两个类似的包Query Key FactoryReact Query KitReact Query Kit比较偏向对queryKeyqueryFn做集中管理,还有中文版的说明,应该是国人开发的,但跟我的需求关联不大,后面项目在好好研究下吧。Query Key Factory更加能满足我对queryKey的管理需求,所以采用了Query Key Factory

Query Key Factory的安装和使用说明

Query Key Factory包的功能说明:

  1. @tanstack/query 的类型安全查询密钥管理,具有自动完成功能。
  2. 专注于编写和使查询无效而无需记住的麻烦
  3. 您如何为特定查询设置密钥! 这个库会处理剩下的事情。

安装:

pnpm add @lukemorales/query-key-factory

以我做的real world第2个版本内容结合官方的例子来做使用说明:

定义queryKey、queryFn

queryKey的定义,官方给了两种方式:

1.  使用`createQueryKeyStore`将所有的`queryKey`都放到一起来定义。官方示例:

    ```jsx
    import { createQueryKeyStore } from "@lukemorales/query-key-factory";

    export const queries = createQueryKeyStore({
      users: {
        // 不定义queryKey和queryFn,会自动产生一个queryKey:['users', 'all']
        all: null,
        detail: (userId: string) => ({
          queryKey: [userId],
          queryFn: () => api.getUser(userId),
        }),
      },
      todos: {
        detail: (todoId: string) => [todoId],
        list: (filters: TodoFilters) => ({
          queryKey: [{ filters }],
          queryFn: (ctx) => api.getTodos({ filters, page: ctx.pageParam }),
          contextQueries: {
            search: (query: string, limit = 15) => ({
              queryKey: [query, limit],
              queryFn: (ctx) => api.getSearchTodos({
                page: ctx.pageParam,
                filters,
                limit,
                query,
              }),
            }),
          },
        }),
      },
    });
    ```

2.  使用`createQueryKeys`,` mergeQueryKeys`组合,通过`createQueryKeys`来定义每个模块的`queryKey`,然后通过`mergeQueryKeys`来合并。我主要使用的这种方式。

    ```jsx
    import { createQueryKeys, mergeQueryKeys } from '@lukemorales/query-key-factory'
    import api from '@/service/apis'

    export const users = createQueryKeys('users', {
      view: () => ({
        queryKey: ['user'],
        queryFn: () => api.User.view()
      })
    })

    export const profile = createQueryKeys('profile', {
      view: username => ({
        queryKey: [username],
        queryFn: () => api.Profile.view(username)
      })
    })

    export const articles = createQueryKeys('articles', {
      all: (limit, offset, author = null, favorited = null, tag = null) => ({
        queryKey: [offset, author, favorited, tag],
        queryFn: () => api.Article.all(limit, offset, author, favorited, tag)
      }),
      feed: (limit, offset) => ({
        queryKey: [offset],
        queryFn: () => api.Article.feed(limit, offset)
      }),
      view: slug => ({
        queryKey: [slug],
        queryFn: () => api.Article.view(slug)
      })
    })

    export const comments = createQueryKeys('comments', {
      all: slug => ({
        queryKey: [slug],
        queryFn: () => api.Comment.all(slug)
      })
    })

    export const tags = createQueryKeys('tags', {
      all: () => ({
        queryKey: ['tags'],
        queryFn: () => api.Default.tags()
      })
    })

    export const querykeys = mergeQueryKeys(users, profile, articles, comments, tags)
    ```

    通过`createQueryKeys`来定义每个模块的`queryKey`和`queryFn`。当然官方的示例也给了只定义`queryKey`在`useQuery`在使用`queryFn`的示例:

    ```jsx
    export const todos = createQueryKeys('todos', {
      detail: (todoId: string) => [todoId],
      list: (filters: TodoFilters) => ({
        queryKey: [{ filters }],
      }),
    });
    ```

    使用可以见后面的官方使用说明

使用定义好的queryKey和queryFn

官方的示例很凌乱,在createQueryKeys时定义的内容不一致,相同的 todo detail有不同的定义,我研究了半天,整理如下:

import { queries } from '../queries'
// 在createQueryKeys中没有定义queryKey、queryFn,但会产生一个默认的queryKey [`users`, 'all']
// 可以在useQuery在来定义queryFn
export function useUsers() {
  return useQuery({
    ...queries.users.all,
    queryFn: () => api.getUsers(),
  });
};

// 使用在createQueryKeys中定义的queryKey、queryFn,直接传递参数即可,会自动生成queryKey、queryFn
// queryKey: ['users', 'detail', userId]
export function useUserDetail(id: string) {
  return useQuery(queries.users.detail(id));
};

因为只需要对useQuery进行QueryKey的设置,我这边的代码如下:

import { useQuery } from '@tanstack/react-query'
import { querykeys } from '@/service/queryKeys'
import { useParams } from 'react-router-dom'

const User = {
  useUser: () => useQuery(querykeys.users.view())
}

const Profile = {
  useProfile: () => {
    const { username } = useParams()
    return useQuery(querykeys.profile.view(username))
  }
}

const Article = {
  useArticles: (limit, offset, author, favorited, tag) =>
    useQuery(querykeys.articles.all(limit, offset, author, favorited, tag)),
  useArticlesFeed: (limit, offset) => useQuery(querykeys.articles.feed(limit, offset)),
  useArticle: (slug, options) => {
    return useQuery(querykeys.articles.view(slug), options)
  }
}

const Comment = {
  useComments: slug => useQuery(querykeys.comments.all(slug))
}

const Tag = {
  useTags: () => useQuery(querykeys.tags.all())
}

export default {
  User,
  Profile,
  Article,
  Comment,
  Tag
}

使用之前定义好的queryKey、queryFn就很简单了。下面放到一起看起来就很清晰。

// queryKey.jsx 定义queryKey、queryFn
export const users = createQueryKeys('users', {
  view: () => ({
    queryKey: ['user'],
    queryFn: () => api.User.view()
  })
})

export const profile = createQueryKeys('profile', {
  view: username => ({
    queryKey: [username],
    queryFn: () => api.Profile.view(username)
  })
})

// queries.jsx 使用queryKey.jsx中定义好的queryKey、queryFn
const User = {
  useUser: () => useQuery(querykeys.users.view())
}

// 如果还有需要查询路径参数的还可以使用react-router-dom
const Profile = {
  useProfile: () => {
    const { username } = useParams()
    return useQuery(querykeys.profile.view(username))
  }
}

这样就可以统一对react-queryqueryKeyqueryFn来进行管理使用了。

如何在useMutation中使用queryKey

使用querykeys.users.view._def中这个方法可以直接使对应的queryKey失效,也不需要传递什么参数。多个模块的查询失效实现也很简单,见下方示例。

或者使用users.detail({ status: 'completed' }).queryKey这种传参数的方式来获取queryKey,再进行queryClient.invalidateQueries操作。

import { useMutation, useQueryClient } from '@tanstack/react-query'
import { querykeys } from '@/service/queryKeys'
import api from '@/service/apis'
import { useAuth } from '@/hooks'

const User = {
  useLogin: () => {
    const { login } = useAuth()
    return useMutation(api.User.login, {
      onSuccess: data => {
        login(data)
      }
    })
  },
  useRegister: () => {
    const { login } = useAuth()
    return useMutation(api.User.register, {
      onSuccess: data => {
        login(data)
      }
    })
  },
  useUserUpdate: () => {
    const queryClient = useQueryClient()
    return useMutation(api.User.update, {
      onSuccess: () => {
        queryClient.invalidateQueries(querykeys.users.view._def)
      }
    })
  }
}

const Profile = {
  useFollow: (following, username) => {
    const queryClient = useQueryClient()
    return useMutation(following ? () => api.Profile.unfollow(username) : () => api.Profile.follow(username), {
      onSuccess: () => {
        queryClient.invalidateQueries(querykeys.profile.view._def)
        queryClient.invalidateQueries(querykeys.articles.view._def)
      }
    })
  }
}

const Article = {
  useArticleCreate: () => {
    const queryClient = useQueryClient()
    return useMutation(api.Article.create, {
      onSuccess: () => {
        queryClient.invalidateQueries(querykeys.articles.all._def)
        queryClient.invalidateQueries(querykeys.articles.feed._def)
      }
    })
  },
  useArticleUpdate: slug => {
    const queryClient = useQueryClient()
    return useMutation(article => api.Article.update(slug, article), {
      onSuccess: () => {
        queryClient.invalidateQueries(querykeys.articles.all._def)
        queryClient.invalidateQueries(querykeys.articles.feed._def)
      }
    })
  },
  useArticleDelete: slug => {
    const queryClient = useQueryClient()
    return useMutation(api.Article.delete, {
      onSuccess: () => {
        queryClient.invalidateQueries(querykeys.articles.all._def)
        queryClient.invalidateQueries(querykeys.articles.feed._def)
      }
    })
  }
}

const Comment = {
  useCommentCreate: slug => {
    const queryClient = useQueryClient()
    return useMutation(body => api.Comment.create(slug, body), {
      onSuccess: () => {
        queryClient.invalidateQueries(querykeys.comments.all._def)
      }
    })
  },
  useCommentDelete: (slug, id) => {
    const queryClient = useQueryClient()
    return useMutation(() => api.Comment.delete(slug, id), {
      onSuccess: () => {
        queryClient.invalidateQueries(querykeys.comments.all._def)
      }
    })
  }
}

const Favorite = {
  useFavorite: (slug, favorited) => {
    const queryClient = useQueryClient()
    return useMutation(favorited ? () => api.Favorite.unfavorite(slug) : () => api.Favorite.favorite(slug), {
      onSuccess: () => {
        queryClient.invalidateQueries(querykeys.articles.all._def)
        queryClient.invalidateQueries(querykeys.articles.feed._def)
        queryClient.invalidateQueries(querykeys.articles.view._def)
      }
    })
  }
}

export default {
  User,
  Profile,
  Article,
  Comment,
  Favorite
}

Query Key Factory还有一些其他的方法,没有细究了,需求满足了。

Query Key Factory 的问题

  1. 问题:在createMutationKeys中创建带有参数的突变,和queryKey一起使用时会undefine,这个bug目前还没解决。New createMutationKeys doesn't work for mutations with arguments · Issue #55 · lukemorales/query-key-factory (github.com)

    解决办法:所以我在项目中没有使用createMutationKeys来创建mutationKeymutationFn。因为useMutation不需要key,使用createMutationKeys只是做个统一管理,可以等后面这个库成熟点了再来看看。

  2. 问题:使用createQueryKeys创建的queryFn不能使用enabled,这个应该不会去解决了。why does enabled not work within the query-key-factory? · Issue #61 · lukemorales/query-key-factory (github.com)

    解决办法:这里可以只在createQueryKeys中需要使用enabled的模块中只声明queryKeyqueryFn可以放到useQuery中。或者不使用createQueryKeys中的queryKey,单独来写useQuery来规避这个问题。