React Query 有三个核心概念:
Queries - 查
Mutations - 增删改
Query Invalidation - 查询失效(重新查询)
TanStack Query 目前支持多种前端框架,我们本章节专注适用于 React 的 React Query 并会在需要做示例的时候使用 Nextjs 示例。
快速上手 我们先以官方文档的 Quick Start 示例来演示三个核心概念:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 import { useQuery, useMutation, useQueryClient, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import { getTodos, postTodo } from '../my-api' const queryClient = new QueryClient()function App ( ) { return ( <QueryClientProvider client ={queryClient} > <Todos /> </QueryClientProvider > ) } function Todos ( ) { const queryClient = useQueryClient() const query = useQuery({ queryKey : ['todos' ], queryFn : getTodos }) const mutation = useMutation({ mutationFn : postTodo, onSuccess : () => { queryClient.invalidateQueries({ queryKey : ['todos' ] }) }, }) return ( <div > {/* query查询的结果可以用来渲染,查询成功后会自动更新 */} <ul > {query.data?.map((todo) => <li key ={todo.id} > {todo.title}</li > )}</ul > <button onClick ={() => { {/* mutation.mutate手动执行一个mutation,用于实现增删改操作,传递的参数会传递给mutationFn */} mutation.mutate({ id: Date.now(), title: 'Do Laundry', }) }} > Add Todo </button > </div > ) } render(<App /> , document .getElementById('root' ))
在示例代码中,我在适当位置做了注释,查看注释即可。
Nextjs 引入 React Query 由于 React Query 需要用于客户端组件,所以我们不能直接在 app/layout.tsx 中注入,我们可以在封装一层组件
1 2 3 4 5 6 7 8 9 10 11 'use client' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' const queryClient = new QueryClient()export default function QueryProvider ({children}: {children: React.ReactNode} ) { return ( <QueryClientProvider client ={queryClient} > {children} </QueryClientProvider > ) }
然后我们在root layout中引入使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 export default function RootLayout ({ children, }: Readonly<{ children: React.ReactNode; }> ) { return ( <html lang ="en" > <body className ={cn( "min-h-screen bg-background font-sans antialiased ", fontSans.variable )}> <QueryProvider > {children} </QueryProvider > </body > </html > ); }
这样就可以在业务组件中使用 React Query 了
查 Query可以和任何基于Promise的方法(包括GET, POST)一起使用,以从服务器获取数据。如果方法修改了服务器的数据,那么建议使用Mutations而不是Query。
使用useQuery至少需要提供:
查询的唯一key - queryKey 这个唯一的key在内部用于整个应用程序重新提取、缓存和共享查询
返回Promise的函数,他应该解析数据或抛出错误 - queryFn
useQuery 返回值 useQuery返回的查询结果有所有需要的信息:
1 const {isPending, isError, isSucess, status, error, data, isFetching} = useQuery()
在任何时刻,查询只能处于以下状态之一:
isPending或者status === 'pending' - 尚无查询数据
isError或者status === 'error' - 查询遇到错误
isSuccess或者status === 'success' - 查询成功且数据可用
除了上边的状态之外,还可以根据状态来获取更多信息:
error - 处于isError状态时可以通过error获取错误
data - 处于isSuccess状态时可以通过data获取错误
isFetching - 如果查询正在获取数据,则isFetching = true
通常只需要检查isPending状态,然后检查isError状态,最后假定数据可用并呈现即可。
检查状态可以用isPending, isError等布尔值,也可以用status,两者取其一即可。
queryKey react-query的核心是根据查询键管理查询缓存。查询键必须是顶层的数组。可以只包含简单的字符串,也可以和嵌套对象的数组一样复杂。只要是可序列化的,并且对于查询的数据是唯一的 ,就可以使用:
queryFn 查询函数可以是返回Promise的任意函数,这个Promise必须返回数据或者抛出错误
queryKey 不仅用于标识想要获取的数据,还可以作为QueryFunctionContext的一部分传递到查询函数中。
1 2 3 4 useQuery({ queryKey : [], queryFn : async (context: QueryFunctionContext) => {} })
QueryFunctionContext是传递给每个查询函数的对象,包括:
queryKey: QueryKey - 查询键
signal?: AbortSignal - 用于取消查询
meta: Record<string, unknown> | undefined - 可以填写有关查询的其他信息
另外,Infinite Queries无限查询会额外传递参数:
pageParam: TPageParam - 提取当前页面的page参数
direction: 'forward' | 'backward' - 页面数据的提取方向
读取queryKey的示例:
1 2 3 4 5 6 7 8 const [page, setPage] = useState(1) useQuery({ queryKey: ['todos', page, { page }, 'others'], queryFn: async ({queryKey}: QueryFunctionContext) => { // ['todos', 1, { page: 1 }, 'others'] console.log(queryKey) } })
接收到的queryKey与定义的完全一致,依然是数组,顺序保持一致。
query options 在多个位置之间共享 queryKey 和 queryFn 但使它们彼此位于同一位置的最佳方法之一是使用 queryOptions 帮助程序。在运行时,这个帮助程序只返回你传递给它的任何内容,但它在与 TypeScript 一起使用时有很多优点。您可以在一个位置定义查询的所有可能选项,并且将获取所有这些选项的类型推断和类型安全
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { queryOptions } from '@tanstack/react-query' function groupOptions (id: number ) { return queryOptions({ queryKey : ['groups' , id], queryFn : () => fetchGroups(id), staleTime : 5 * 1000 , }) } useQuery(groupOptions(1 )) useQuery(groupOptions(5 ))
对于无限查询,可以使用单独的infiniteQueryOptions帮助程序。
并行查询 并行执行查询,最大限度提高查询的并发性。
手动并行查询 当并行查询数量没有变化时,不需要做任何工作,只要并排使用任意数量的useQuery或者useinfiniteQuery即可
1 2 3 4 const usersQuery = useQuery()const teamsQuery = useQuery()const projectsQuery = useQuery()...
使用useQueries进行动态并行查询 当需要执行的查询数量在不同渲染之间会发生变化,那么不能用手动查询了,可以使用useQueries这个hook,他可以动态执行任意数量的查询。
useQueries接收一个带有queries字段的对象,其值是一个查询对象数组。返回查询结果数组:
1 2 3 4 5 6 7 8 9 10 function App({ users }) { const userQueries = useQueries({ queries: users.map(user => { return { queryKey: ['user', user.id], queryFn: () => fetchUserById(user.id) } }) }) }
这里用的是props来动态生成的请求查询,当然可以根据state或者其他来动态生成查询。
串行查询 依赖于先前的查询在执行之前完成。
useQuery 依赖查询 只需要使用enabled来告知查询何时准备好运行即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const { data : user } = useQuery({ queryKey : ['user' , email], queryFn : getUserByEmail }) const userId = user?.idconst {} = useQuery({ queryKey : ['projects' , userId], queryFn : getProjectsByUser, enabled : !!userId, })
useQueries依赖查询 useQueries也可以依赖于以前的查询,实现方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const { data : userIds } = useQuery({ queryKey : ['users' ], queryFn : getUsersData, select : (users ) => users.map(user => user.id), }) const usersMessage = useQueries({ queries : userIds ? userIds.map(id => { return { queryKey : ['message' , id], queryFn : () => getMessageByUsers(id) } }) : [] })
初始数据 使用 config.initialData 选项来设置查询的初始数据并跳过初始加载状态
1 2 3 4 5 const result = useQuery({ queryKey : ['todos' ], queryFn : () => fetch('/todos' ), initialData : initialTodos, })
initialData将保存在缓存中,因此不建议向此选项提供占位符或不完整数据,而应该使用placeholderData
占位数据 类似于 initialData 选项,但数据不会保存到缓存中 。当您有足够的部分(或虚假)数据来成功呈现查询,而实际数据是在后台获取时,这会派上用场
1 2 3 4 5 6 7 function Todos ( ) { const result = useQuery({ queryKey : ['todos' ], queryFn : () => fetch('/todos' ), placeholderData : placeholderTodos, }) }
如果访问查询占位符数据的过程非常密集,或者您不想在每次渲染时都执行,则可以记住该值:
1 2 3 4 5 6 7 8 function Todos() { const placeholderData = useMemo(() => generateFakeTodos(), []) const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), placeholderData, }) }
placeholderData 也可以是一个函数,您可以在其中访问“上一个”成功 Query 的数据和 Query 元信息。对于希望将一个查询中的数据用作另一个查询的占位符数据的情况,这很有用。当 QueryKey 发生变化时,例如从 [‘['todos', 1], ['todos', 2],我们可以继续显示“旧”数据,而不必在数据从一个查询转换 到下一个查询时显示加载器。
1 2 3 4 5 const result = useQuery({ queryKey: ['todos', id], queryFn: () => fetch(`/todos/${id}`), placeholderData: (previousData, previousQuery) => previousData, })
在某些情况下,您可以根据另一个查询的缓存结果提供占位符数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function Todo({ blogPostId }) { const queryClient = useQueryClient(); const result = useQuery({ queryKey: ['blogPost', blogPostId], queryFn: () => fetch(`/blogPosts/${blogPostId}`), placeholderData: () => { // Use the smaller/preview version of the blogPost from the 'blogPosts' // query as the placeholder data for this blogPost query return queryClient .getQueryData(['blogPosts']) ?.find((d) => d.id === blogPostId) }, }) }
分页查询 分页数据是非常常见的,在React Query中通过在查询键中包含分页信息来实现:
1 2 3 4 const result = useQuery({ queryKey: ['projects', page], queryFn: fetchProjects })
这样确实可以工作,但是会发生一些奇怪的情况,页码改变时UI 跳入和跳出success和pending状态,因为每个新页面都被视为一个全新的查询。
这种体验是不佳的,React Query中有名为placeholderData的功能,可以解决这个问题。
思路:
在请求新数据时,上次成功提取的数据可用,即使查询键已更改也是如此 .
当新数据到达时,以前的数据将无缝交换以显示新data。
isPlaceholderData 可用于了解查询当前为您提供的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import { keepPreviousData, useQuery } from "@tanstack/react-query" ;import React from "react" ;function Todos ( ) { const [page, setPage] = React.useState(0 ) const fetchProjects = (page = 0 ) => fetch('/api/projects?page=' + page).then((res ) => res.json()) const { isPending, isError, error, data, isFetching, isPlaceholderData, } = useQuery({ queryKey : ['projects' , page], queryFn : () => fetchProjects(page), placeholderData : keepPreviousData, }) return ( <div > {isPending ? ( <div > Loading...</div > ) : isError ? ( <div > Error: {error.message}</div > ) : ( <div > {data.projects.map(project => ( <p key ={project.id} > {project.name}</p > ))} </div > )} <span > Current Page: {page + 1}</span > <button onClick ={() => setPage(old => Math.max(old - 1, 0))} disabled={page === 0} > Previous Page </button > {' '} <button onClick ={() => { if (!isPlaceholderData && data.hasMore) { setPage(old => old + 1) } }} disabled={isPlaceholderData || !data?.hasMore} > Next Page </button > {isFetching ? <span > Loading...</span > : null}{' '} </div > ) }
将keepPreviousData设置为placeholderData,会在之后的请求过程中用当前的数据进行占位。
通过isPlaceholderData判断是否是占位数据
关于 Query 的内容就先说这么多吧,这些是常用内容,当然 React Query 支持更多的配置项和操作,自己查阅文档吧
增删改 与Query不同,Mutation通常用于 增删改 或 执行服务器副作用。使用useMutation这个hook。
在任何给定时刻,Mutation只能处于以下状态之一:
isIdle or status === 'idle' - 当前处于空闲状态或处于全新/重置状态
isPending 或 status === 'pending' - 当前正在运行
isError 或 status === 'error' - 遇到错误
isSuccess 或status === 'success' - 成功且数据可用
除了这些主要状态之外,根据Mutation的状态,还可以获得更多信息:
error - 如果处于错误状态,则可通过 error 属性获取该error。
data - 如果处于success状态,则可通过 data 属性获取数据
即使只有变量,Mutation也不是那么特别,但是当与 onSuccess 选项、查询客户端的 invalidateQueries 方法和查询客户端的setQueryData 方法一起使用时,Mutation会成为一个非常强大的工具。
基础使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const CreateTodo = () => { const [title, setTitle] = useState('' ) const mutation = useMutation({ mutationFn : createTodo }) const onCreateTodo = (e ) => { e.preventDefault() mutation.mutate({ title }) } return ( <form onSubmit ={onCreateTodo} > {mutation.error && ( /*使用reset方法重置*/ <h5 onClick ={() => mutation.reset()}>{mutation.error}</h5 > )} <input type ="text" value ={title} onChange ={(e) => setTitle(e.target.value)} /> <br /> <button type ="submit" > Create Todo</button > </form > ) }
回调 在调用 mutate 时,除了 useMutation 上定义的回调之外,还希望触发其他回调 。这可用于触发特定于组件的副作用。为此,您可以在 mutation 变量之后为 mutate 函数提供任何相同的回调选项。支持的选项包括:onSuccess、onError 和 onSettled。请记住,如果您的组件在变更完成之前 卸载,这些额外的回调将不会运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 const mutation = useMutation({ mutationFn : addTodo, onSuccess : (data, variables, context ) => { }, onError : (error, variables, context ) => { }, onSettled : (data, error, variables, context ) => { }, }) mutation.mutate(todo, { onSuccess : (data, variables, context ) => { }, onError : (error, variables, context ) => { }, onSettled : (data, error, variables, context ) => { }, })
查询失效 当您知道查询的数据由于用户执行的操作而过期时。为此,QueryClient 有一个 invalidateQueries 方法,该方法允许您智能地将查询标记为过时,并可能重新获取它们
1 2 3 4 queryClient.invalidateQueries() queryClient.invalidateQueries({ queryKey : ['todos' ] })
查询匹配
使查询键中以指定内容开头的查询无效
1 2 3 4 5 6 7 8 9 10 11 const queryClient = useQueryClient()queryClient.invalidateQueries({ queryKey : ['todos' ] }) useQuery({ queryKey : ['todos' ], queryFn : , }) useQuery({ queryKey : ['todos' , { page : 1 }], queryFn : , })
传递更具体的查询键
1 2 3 4 5 6 7 8 9 10 11 12 const queryClient = useQueryClient()queryClient.invalidateQueries({ queryKey : ['todos' , { type : 'done' } ] }) useQuery({ queryKey : ['todos' , { type : 'done' }], queryFn : , }) useQuery({ queryKey : ['todos' ], queryFn : , })
严格匹配,传递exact: true选项,保证匹配仅有传递的查询键不包括更多变量的查询
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 queryClient.invalidateQueries({ queryKey : ['todos' ], exact : true , }) const todoListQuery = useQuery({ queryKey : ['todos' ], queryFn : fetchTodoList, }) const todoListQuery = useQuery({ queryKey : ['todos' , { type : 'done' }], queryFn : fetchTodoList, })
传递predicate函数实现更细粒度控制,此函数从缓存中接受每个Query实例,允许返回true / false决定是否失效
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 queryClient.invalidateQueries({ predicate : (query ) => { return query.queryKey[0 ] === 'todos' && query.queryKey[1 ]?.version >= 10 } }) useQuery({ queryKey : ['todos' , { version : 20 }], queryFn : , }) useQuery({ queryKey : ['todos' , { version : 10 }] }) useQuery({ queryKey : ['todos' , { version : 5 }] })
Mutation导致的失效 通常,当应用中的mutation执行成功时,应用程序中很可能存在需要失效并可能重新提取的相关查询,以考虑带来的新更改。
为此,您可以使用 useMutation 的 onSuccess 选项和client的 invalidateQueries 函数
1 2 3 4 5 6 useMutation({ mutationFn: , onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }) } })
Nextjs示例 由于目前还没接触到数据库相关内容,做示例太过于局限,所以决定在下一章节讲 Prisma 与 Nextjs 的配合使用,然后基于讲过的内容做一个相对简单但是增删改查覆盖全面的示例。