小伙伴们,是时候开始 React Query 之旅了。你还不知道这个库吗?完美,你来对地方了 😁
介绍
React Query 是什么?React Query 是由@TannerLinsley 创建的 npm 库。它是一个针对 React 应用的状态管理器,可以简化许多任务,例如处理 HTTP 请求状态、在客户端保存数据以防止多次请求、使用 hooks 共享数据等等。
你将在本系列中发现更多关于它的内容,学习如何使用它,并欣赏其在 React 应用程序中的简洁性。
useQuery
第一个核心概念是 useQuery。通过它,你可以以一种非常简单的方式从源中检索数据并处理此请求的所有状态。
让我们看一个例子:
import { useQuery } from '@tanstack/react-query'; const fetchTodos = async (): Promise<Todo[]> => { const response = await fetch('api/tasks'); if (!response.ok) { throw new ResponseError('Failed to fetch todos', response); } return await response.json(); }; export const useTodos = (): UseTodos => { const { data: todos = [], isLoading, isFetching, error, } = useQuery(['todos'], fetchTodos, { refetchOnWindowFocus: false, retry: 2, }); ... };
在这个例子中,你可以看到 useQuery 的要点。
UseQuery 是一个 React hook,它需要三个参数:
1.查询关键字
2.查询函数
3.配置项
让我们从第一个参数开始。查询关键字是 React Query 用于识别你的查询的关键字。通过该关键字,React Query 能够存储结果并在应用程序的不同部分中使用它。该关键字用于标识查询,你还可以使用 React Query 客户端通过代码重置查询或更改值。
查询函数是用于从源(rest、GraphQL 等等)检索数据的方法。它很简单,一个返回某种数据的函数,可以是简单函数或者大多数情况下是一个 promise。
然后是配置项,这些很简单啦 :) 有许多可能的选项用于以不同的方式运行查询(重试次数、何时刷新数据、如何缓存数据等等..)。
这个 hook 的结果有三个重要的属性:
- data:此属性包含查询函数的结果。请注意数据也可能为 undefined;这是因为在第一次调用时,当请求处于等待状态时,data 尚未呈现。
- isLoading:这个标志表示 React Query 正在加载数据。还有一个 isFetching 标志,如果你正在创建无限滚动,则很重要。isFetching 标志表示有一个挂起的请求,如果应用程序请求下一个信息,这是非常完美的。
- error:此对象包含请求存在问题的错误;通过使用它,你可以获取错误并为用户创建漂亮的信息提示。
好的,你现在对 useQuery 的工作方式及其潜力有了一个概念,但是如果你更有兴趣,可以观看我的视频了解更多信息。
好的,就这些!我很快会回到你呈现 React Query 的另一个功能。希望你喜欢这份内容。
突变
伙计们,是时候谈论 React Query 中的第二个核心概念了,即突变。
这是什么?
突变是用户可以在你的应用程序中执行的操作,你可以将突变想象成更改或创建某些东西的操作。
为了更好地在代码中理解突变是什么,让我们从一个代码片段开始
import { useMutation } from '@tanstack/react-query'; const postTodo = async (text: Todo['text']): Promise<Todo> => { const response = await fetch('api/tasks', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ text }), }); if (!response.ok) { throw new ResponseError('Failed to insert new todo', response); } return await response.json(); }; export const useAddTodo = (): UseAddTodo => { const { mutate: addTodo, isLoading, error } = useMutation(postTodo, { onSuccess: () => { // Success actions }, onError: (error) => { // Error actions }, }); return { addTodo, }; };
正如你所看到的,突变是一个简单的 hook,有两个参数:
- 用于处理请求的函数
- 用于处理成功和错误 hooks 的选项,但也用于配置突变(重试、重试延迟等)。
结果有三个主要的对象:
- mutate:这是在你的代码中运行突变的操作
- isLoading:这个标志表示突变是否正在进行
- error:这表示如果请求出现错误,则显示错误
在 React 应用程序中使用突变,你可以处理所有那些操作来改变数据并简化这些请求的状态管理。
当你处理突变时,另一个重要的概念是 QueryClient。
使用 QueryClient,你可以使已经提供的查询失效,并告诉 React Query 重新请求数据,因为你可以确保在突变之后,那些数据还不是有效的。
为了这样做,你必须使用 useQueryClient 钩子来检索 queryClient,并使用 invalidateQueries 方法,你可以使 React Query 缓存无效,同时使指定的查询或多个查询失效。
以下是一个例子
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { QUERY_KEY } from '../../../../constants/queryKeys'; export const useAddTodo = (): UseAddTodo => { const client = useQueryClient(); const { mutate: addTodo } = useMutation(postTodo, { onSuccess: () => { client.invalidateQueries([QUERY_KEY.todos]); }, }); ... };
好的,我想你已经对如何使用 useMutation 和 useQueryClient 有了一个概念,但是如果你想深入了解它们,请别忘了看我的 Youtube 视频。
React Query 提供的两个 hooks:useIsFetching 和 useIsMutation。
这些 hooks 可用于了解应用程序中是否存在获取请求或突变请求正在进行。
如果需要创建一个全局的加载器,在存在一个或多个请求进行时出现,它们就会很有用。
但是你如何使用它们呢?
我们先从 useIsFetching 开始。
import { useIsFetching } from '@tanstack/react-query'; export default function Loader() { const isFetching = useIsFetching(); if (!isFetching) return null; return <>Fetching...</> }
正如你所看到的,语法非常简单。你可以从库中导入该 hook 并在组件中使用。该 hook 仅返回一个布尔值,表示应用程序中是否存在一个或多个获取请求。因此,你可以根据这些数据决定是否显示加载器。Easy peasy!
现在是时候移动到 useIsMutation hook 了。这个 hook 类似于之前的那个,唯一不同的概念是这个 hook 处理的是突变请求。让我们看一个例子!
import { useIsMutating } from '@tanstack/react-query'; export default function Loader() { const isMutating = useIsMutating(); if (!isMutating) return null; return <>Mutating...</> }
正如你所注意到的那样,语法与之前的相同,唯一不同的是 hook 的名称和其概念。
Dev tool
接下来,你将学习如何调试和检查 React Query 应用程序中发生的一切。当你开始学习或使用一个工具时,检查它周围的工具以了解开发者体验是很正常的,这样你就可以决定是否继续使用它。React Query 团队知道这一点,并决定构建一个工具来帮助那些想要使用 React Query 进行工作的开发者。
这个工具叫做react-query-devtools
,你只需要通过一个简单的步骤安装它。
打开你的终端并输入
$ npm i @tanstack/react-query-devtools
现在,在你的项目中,你可以使用它并得到所有需要调试你的应用程序所需的信息。
这个工具很容易使用。在你的应用程序中,你必须将它导入并在你渲染ReactQueryProvider
的地方渲染它。
import { QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import React from "react"; import { queryClient } from './react-query/client'; import Router from './Router'; function App() { return ( <React.StrictMode> <QueryClientProvider client={queryClient}> ... <ReactQueryDevtools /> </QueryClientProvider> </React.StrictMode> ); } export default App;
Easy peasy, no? 😎
使用ReactQueryDevtools
,你不需要关注环境是否渲染该组件,因为它默认提供了它。它仅在条件process.env.NODE_ENV === 'development'
为 true 时才渲染该组件。
如果需要,你可以自定义该组件或强制在生产模式下渲染它。要了解更多相关主题,请查阅文档。
在你的应用程序中使用该组件的好处在于,它允许在运行时查看 ReactQuery 中发生的情况。你可以检查状态中保存的数据,不同的查询有多少应用程序部分使用等等。你也可以重置状态或删除部分状态以重新获取数据。
没错,它提供了许多很好的功能来调试和检查你的 React Query 应用程序,并且它是每个使用 React Query 的开发者的好工具。在这里,你可以找到一个 ReactQueryDevtool 的示例。
权限
每个应用程序都应该处理认证流程;在这篇文章中,你将学习如何使用 React Query 在你的 React 应用程序中构建认证流程。
注册
构建认证流程的第一步是注册操作。通过本系列你已经学习到,你应该构建一个 mutation 来执行此操作。一种可能的解决方法如下:
async function signUp(email: string, password: string): Promise<User> { const response = await fetch('/api/auth/signup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) }) if (!response.ok) throw new ResponseError('Failed on sign up request', response); return await response.json(); } type IUseSignUp = UseMutateFunction<User, unknown, { email: string; password: string; }, unknown> export function useSignUp(): IUseSignUp { const queryClient = useQueryClient(); const navigate = useNavigate(); const { enqueueSnackbar } = useSnackbar(); const { mutate: signUpMutation } = useMutation<User, unknown, { email: string, password: string }, unknown>( ({ email, password }) => signUp(email, password), { onSuccess: (data) => { // TODO: save the user in the state navigate('/'); }, onError: (error) => { enqueueSnackbar('Ops.. Error on sign up. Try again!', { variant: 'error' }); } }); return signUpMutation; }
通过创建这样的 mutation,你可以非常简单和清晰地构建一个注册操作。
现在使用 useSignUp hook,你可以获取 mutation 并调用 signUp 请求在你的系统中创建新用户。正如你可以看到的,代码非常简单,signUp 方法调用 API 来发布新用户的数据并返回保存在数据库中的用户数据。然后使用 useMutation hook,可以构建处理 signUp 操作的 mutation。如果一切正常,onSuccess hook 调用导航到主页;否则,onError hook 显示一个错误的提示。
在代码中,有一个 TODO 表示缺失的内容;我们将在此后的文章中回到这行代码。
登录
如果你正在建立一个身份验证流程,那么 SignIn 是构建的第二个步骤。在这种情况下,SignIn 与 SignUp 非常相似;唯一变化的是终点和 Hook 的范围。
所以代码可以是这样的:
async function signIn(email: string, password: string): Promise<User> { const response = await fetch('/api/auth/signin', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) }) if (!response.ok) throw new ResponseError('Failed on sign in request', response); return await response.json(); } type IUseSignIn = UseMutateFunction<User, unknown, { email: string; password: string; }, unknown> export function useSignIn(): IUseSignIn { const queryClient = useQueryClient(); const navigate = useNavigate(); const { enqueueSnackbar } = useSnackbar(); const { mutate: signInMutation } = useMutation<User, unknown, { email: string, password: string }, unknown>( ({ email, password }) => signIn(email, password), { onSuccess: (data) => { // TODO: save the user in the state navigate('/'); }, onError: (error) => { enqueueSnackbar('Ops.. Error on sign in. Try again!', { variant: 'error' }); } }); return signInMutation; }
用户
身份验证流程的核心部分是将用户保存在状态中。为了做到这一点,在这种情况下,最好的方法是创建一个称为 useUser 的新 hook,它是用户数据的所有者。
useUser hook 必须具有用户数据,并且它必须将用户数据保存在本地存储中,并在以后刷新页面或返回时检索它们。
先从处理本地存储的代码开始,通常使用具有特定目标的小功能创建此代码,例如:
import { User } from './useUser'; const USER_LOCAL_STORAGE_KEY = 'TODO_LIST-USER'; export function saveUser(user: User): void { localStorage.setItem(USER_LOCAL_STORAGE_KEY, JSON.stringify(user)); } export function getUser(): User | undefined { const user = localStorage.getItem(USER_LOCAL_STORAGE_KEY); return user ? JSON.parse(user) : undefined; } export function removeUser(): void { localStorage.removeItem(USER_LOCAL_STORAGE_KEY); }
以这种方式,您可以创建一个处理用户的所有本地存储函数的小模块。
现在是时候看看如何构建 useUser hook 了。
先从以下代码开始:
async function getUser(user: User | null | undefined): Promise<User | null> { if (!user) return null; const response = await fetch(`/api/users/${user.user.id}`, { headers: { Authorization: `Bearer ${user.accessToken}` } }) if (!response.ok) throw new ResponseError('Failed on get user request', response); return await response.json(); } export interface User { accessToken: string; user: { email: string; id: number; } } interface IUseUser { user: User | null; } export function useUser(): IUseUser { const { data: user } = useQuery<User | null>( [QUERY_KEY.user], async (): Promise<User | null> => getUser(user), { refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false, initialData: userLocalStorage.getUser, onError: () => { userLocalStorage.removeUser(); } }); useEffect(() => { if (!user) userLocalStorage.removeUser(); else userLocalStorage.saveUser(user); }, [user]); return { user: user ?? null, } }
getUser 函数很简单,它提供获取用户信息的 HTTP 请求;如果用户为空,则返回 null,否则调用 HTTP 终点。
useQuery hook 与之前看到的其他 hook 类似,但有两个新配置需要了解。
- refetchOnMount:此选项很重要,可防止 hook 每次使用时重新加载数据
- initialData:此选项用于从本地存储加载数据;initialData 接受一个返回初始值的函数;如果初始值已定义,则 React Query 使用该值刷新数据。
现在您具备了身份验证流程的所有块,但是现在是将 useSignUp 和 useSignIn 与 useUser hook 链接起来的时候了。
使用 QueryClient,您可以使用 setQueryData 函数设置特定查询的数据。
因此,以以下方式更改以前的 TODOs 注释:
export function useSignUp(): IUseSignUp { const queryClient = useQueryClient(); const navigate = useNavigate(); const { enqueueSnackbar } = useSnackbar(); const { mutate: signUpMutation } = useMutation<User, unknown, { email: string, password: string }, unknown>( ({ email, password }) => signUp(email, password), { onSuccess: (data) => { queryClient.setQueryData([QUERY_KEY.user], data); navigate('/'); }, onError: (error) => { enqueueSnackbar('Ops.. Error on sign up. Try again!', { variant: 'error' }); } }); return signUpMutation; } export function useSignIn(): IUseSignIn { const queryClient = useQueryClient(); const navigate = useNavigate(); const { enqueueSnackbar } = useSnackbar(); const { mutate: signInMutation } = useMutation<User, unknown, { email: string, password: string }, unknown>( ({ email, password }) => signIn(email, password), { onSuccess: (data) => { queryClient.setQueryData([QUERY_KEY.user], data); navigate('/'); }, onError: (error) => { enqueueSnackbar('Ops.. Error on sign in. Try again!', { variant: 'error' }); } }); return signInMutation; }
只需两行简单的代码,您就可以将用户设置到 useUser 状态中,因为设置查询数据的键与 useUser 相同。
然后,使用 useUser hook 中的 useEffect,可以在用户更改时删除或设置用户数据到本地存储中:
export function useUser(): IUseUser { const { data: user } = useQuery<User | null>( [QUERY_KEY.user], async (): Promise<User | null> => getUser(user), { refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false, initialData: userLocalStorage.getUser, onError: () => { userLocalStorage.removeUser(); } }); useEffect(() => { if (!user) userLocalStorage.removeUser(); else userLocalStorage.saveUser(user); }, [user]); return { user: user ?? null, } }
要完成身份验证流程,唯一缺少的是注销。
可以使用一个名为 useSignOut 的自定义 hook 来构建它;它的实现很简单,如下所示:
import { useQueryClient } from '@tanstack/react-query'; import { useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { QUERY_KEY } from '../constants/queryKeys'; type IUseSignOut = () => void export function useSignOut(): IUseSignOut { const queryClient = useQueryClient(); const navigate = useNavigate(); const onSignOut = useCallback(() => { queryClient.setQueryData([QUERY_KEY.user], null); navigate('/auth/sign-in'); }, [navigate, queryClient]) return onSignOut; }
正如您可以注意到的那样,hook 返回一个简单的函数,该函数清除用户状态中的值并导航到登录页面。
好的,完美。现在您已具备使用 React Query 构建身份验证流程的所有知识!