:::info ℹ️ 这篇文章,大部分从官方文档搬过来,作为手册速查。 :::
什么是 ReactQuery
React Query is often described as the missing data-fetching library for React, but in more technical terms, it makes fetching, caching, synchronizing and updating server state in your React applications a breeze.
- ReactQuery 提供了服务端状态的:拉取、同步、更新操作的 Hook 化封装:
- Query 缓存(前端提升性能最重要的策略)🥫
- 请求依赖(并行、串行依赖)解耦,可以将多个请求依赖,转化为一个请求依赖
- 增加了请求是否「过期」的判定,可以增加不同机制的过期判定
- 直接关联 ReactComponent 状态,立刻生效而无需额外一行代码
- 内置支持分页、懒加载、长轮询、无限滚动等场景
- 管理内存和垃圾数据集的回首操作
- 支持按照 query 格式存储结构化的信息
- …
- 处理
ServerSideState
在客户端的管理,能够实现ServerSate
和ClientState
解耦。 - ReactQuery 和 Redux 之类的状态库的关系比较微妙,通过剥离「SeverState」,发现 ClientState 比较少的话,其实 Redux 等状态管理库使用意义也就不大了。可以通过 ReactQuery 来将这些 ServerSide 轻松用少量的代码就能够管理到极致。
This also means that with a few hook calls to useQuery and useMutation, we also get to remove any boilerplate code that use to manage our server state.
e.g.
- Connectors
- Action Creators
- Middlewares
- Reducers
- Loading/Error/Result states
- Contexts
With all of those things removed, you may ask yourself, “Is it worth it to keep using our client state manager for this tiny global state?” And that’s up to you! But React Query’s role is clear. It removes asynchronous wiring and boilerplate from your application and replaces it with just a few lines of code.
建议有时间可以看一下作者的介绍视频 👉 https://www.youtube.com/watch?v=seU46c6Jz7E&t=28s
理解 Query 的四个状态
在 ReactQuery 的 DevConsole 里面可以看到 Query 的调试状态:
- fresh:当前 Query 是「新鲜」的,重复命中 Query 不会重复发起请求
- fetching:当前 Query 是正在执行请求中
- stale:当前 Query 已然过期了,下次执行 Query 会发送请求
- inactive:当前 Query 没有激活,5 分钟后会被回收掉(从内存中移除)
全 API 展示
useQuery 查询类状态
const query = useQuery('myAPIName', api.returnPromise, {
// 初始化数据
initialData: {},
// 和上面初始化数据类似,但不会被引擎缓存
placeholder: {}
});
// queryObject
const {
status, // loading | error | success | idle
isLoading,
isError,
// not start request yet
isIdle,
isSuccess,
// In any state, if the query is fetching at any time (including background refetching) isFetching will be true
isFetching,
error, // reject error
data,
} = query;
// queryCacheByKey
// Query Keys are hashed deterministically!
const q1 = useQuery('myAPIName', () => axios.get());
const q2 = useQuery(['myAPIName', reqParams, appState],
() => axios.get(reqParams, appState));
useMutation 非查询类型变更
数据变更,非 Query(查询)类请求封装。
const CreateTodo = () => {
const [title, setTitle] = useState('')
const mutation = useMutation(createTodo, {
onMutate: variables => {
// A mutation is about to happen!
// Optionally return a context containing data to use when for example rolling back
return { id: 1 }
},
onError: (error, variables, context) => {
// An error happened!
console.log(`rolling back optimistic update with id ${context.id}`)
},
onSuccess: (data, variables, context) => {
// Boom baby!
},
onSettled: (data, error, variables, context) => {
// Error or success... doesn't matter!
},
});
const onCreateTodo = e => {
e.preventDefault()
mutation.mutate({ title })
}
return (
<form onSubmit={onCreateTodo}>
{mutation.error && (
<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>
)
}
Parallel & Dependent Queries 并行和串行依赖查询
// Dynamic Parallel Queries
function App({ users }) {
const userQueries = useQueries(
users.map(user => {
return {
queryKey: ['user', user.id],
queryFn: () => fetchUserById(user.id),
}
})
)
}
// Dependent Queries
// Get the user
const { data: user } = useQuery(['user', email], getUserByEmail)
const userId = user?.id
// Then get the user's projects
const { isIdle, data: projects } = useQuery(
['projects', userId],
getProjectsByUser,
{
// The query will not execute until the userId exists
enabled: !!userId,
}
)
// isIdle will be `true` until `enabled` is true and the query begins to fetch.
// It will then go to the `isLoading` stage and hopefully the `isSuccess` stage :)
Disabling/Pausing Queries
当 queryConfig 中 enabled
为 false
时。
- If the query has cached data
- The query will be initialized in the status === ‘success’ or isSuccess state.
- If the query does not have cached data
- The query will start in the status === ‘idle’ or isIdle state.
- The query will not automatically fetch on mount.
- The query will not automatically refetch in the background when new instances mount or new instances appearing
- The query will ignore query client invalidateQueries and refetchQueries calls that would normally result in the query refetching.
- refetch can be used to manually trigger the query to fetch.
RefreshQuery 刷新请求
invalidateQueries 数据标记需要更新
- 使用
invalidateQueries(queryName)
直接标记,标记后对于 Active 状态的请求会重新请求并连接对应视图做自动刷新。
// Access the client
const queryClient = useQueryClient()
// Queries
const query = useQuery('todos', getTodos)
// Mutations
const mutation = useMutation(postTodo, {
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries('todos')
// 设置全部请求 Query 失效
queryClient.invalidateQueries()
// 设置带参数的请求 Query 失效
queryClient.invalidateQueries(['todos', { type: 'done' }])
// 支持复合条件表达式
queryClient.invalidateQueries({
predicate: query =>
query.queryKey[0] === 'todos' && query.queryKey[1]?.version >= 10,
})
// 手动请求 Query
queryClient.refreshQuery('todos')
},
})
refetchQueries 重新拉取
- 调用 QueryClient 实例上的重新请求方法:
const query = useQuery(['todos', 'undo', 1], getTodos);
const queryClient = useQueryClient()
queryClient.refetchQueries(['todos', 'undo', 2]);
cancelQuery 取消请求发送
通过在返回的 promise 对象上,增加一个 cancel 函数,做取消的回调函数:
const [queryKey] = useState('todos')
const query = useQuery(queryKey, () => {
const controller = new AbortController()
const signal = controller.signal
const promise = fetch('/todos', {
method: 'get',
signal,
})
// Cancel the request if React Query calls the `promise.cancel` method
promise.cancel = () => controller.abort()
return promise
})
const queryClient = useQueryClient();
return (
<button onClick={(e) => {
e.preventDefault();
queryClient.cancelQueries(queryKey);
}}>Cancel</button>
)
最佳实践
QueryClient 配置
/**
* ReactQuery
* React Query is often described as the missing data-fetching library for React, but in more technical terms,
* it makes fetching, caching, synchronizing and updating server state in your React applications a breeze.
* @doc https://react-query.tanstack.com/overview
* @notice 用好 ReactQuery 可以提升应用性能并且提升代码优雅度
*/
const queryClient = new QueryClient({
defaultOptions: {
queries: {
/**
* React 节点挂载时是否重新请求
*/
refetchOnMount: true,
/**
* 系统网络重连之后是否重新请求
*/
refetchOnReconnect: true,
/**
* 请求是否需要在固定间隔时间内再次发起
* @default false
*/
refetchInterval: false,
/**
* 当 WindowFocus 事件激活的时候是否重新请求
*/
refetchOnWindowFocus: false,
/**
* 声明 staleTime 可以让特定时间的缓存生效,默认 5 分钟,提升性能,如果需要手动更新,请在 useQuery() 或 useInfiniteQuery() 的参数中调整
* @notice Query instances via useQuery or useInfiniteQuery by default consider cached data as stale.
* @example
* ```typescript
* const q = useQuery('todos', api.getTodo, () => { queryClient.invalidateQueries('todos') });
*
*/
staleTime: STALE_TIME,
/**
* 请求失败后重试次数
*/
retry: RETRY_TIMES,
/**
* Query results by default are structurally shared to detect if data has actually changed and if not,
* the data reference remains unchanged to better help with value stabilization with regards to
* useMemo and useCallback. If this concept sounds foreign, then don't worry about it! 99.9%
* of the time you will not need to disable this and it makes your app more performant at zero cost to you.
*/
structuralSharing: true,
/**
* 统一报错入口
*/
onError(error) {
if (error) {
console.error(error as Error);
}
// 可以在这里做错误的统一处理 & 拦截
},
},
mutations: {
retry: RETRY_TIMES,
retryDelay: 2000,
onError(error) {
if (error) {
console.error(error as Error);
}
// 可以在这里做错误的统一处理 & 拦截
},
},
}, });
<a name="txCyX"></a>
## 分页场景 PaginatedQuery
:::info
✅ 真正意义上让分页变得非常简单了 👍(同时 Cache 了数据)
:::
[https://react-query.tanstack.com/guides/paginated-queries](https://react-query.tanstack.com/guides/paginated-queries)
```typescript
function Todos() {
const [page, setPage] = React.useState(0)
const fetchProjects = (page: number) => axios.get('/todos');
const {
isLoading,
isError,
error,
data,
isFetching,
isPreviousData,
} = useQuery(['projects', page], () => fetchProjects(page), {
keepPreviousData : true
});
return (
<div>
{isLoading ? (
<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 (!isPreviousData && data.hasMore) {
setPage(old => old + 1)
}
}}
// Disable the Next Page button until we know a next page is available
disabled={isPreviousData || !data?.hasMore}
>
Next Page
</button>
{isFetching ? <span> Loading...</span> : null}{' '}
</div>
)
}
无限滚动加载场景
https://react-query.tanstack.com/guides/infinite-queries
import { useInfiniteQuery } from 'react-query'
function Projects() {
const fetchProjects = ({ pageParam = 0 }) =>
fetch('/api/projects?cursor=' + pageParam)
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,
} = useInfiniteQuery('projects', fetchProjects, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
return status === 'loading' ? (
<p>Loading...</p>
) : status === 'error' ? (
<p>Error: {error.message}</p>
) : (
<>
{data.pages.map((group, i) => (
<React.Fragment key={i}>
{group.projects.map(project => (
<p key={project.id}>{project.name}</p>
))}
</React.Fragment>
))}
<div>
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
</button>
</div>
<div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
</>
)
}
启用 DevTools
使用 DevTools 可以观察和处理 ReactQuery 里面每一次数据请求的状态、新鲜度(是否需要更新)、数据结构等等。
import { ReactQueryDevtools } from 'react-query/devtools'
function App() {
return (
<QueryClientProvider client={queryClient}>
{/* The rest of your application */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
Hooks 封装最佳实践
将 useQuery / useMutation 等函数,封装为可复用的,具备业务语义的 Hooks,可以降低和 UI React 函数的耦合性,提升代码的优雅程度。
// useTodoListQuery.ts
export const useTodoList = (id: ) => {
const query = useQuery('todos', () => axios.get('/api/todos'));
const detailQuery = useQuery(
'todoDetail',
(id) => axios.get(`/api/todo/${id}`),
{
enabled: !!id
}
);
const { data, error, isError } = query;
return {
originalQuery: query,
todoList: data.todo.filter(i => i.invalid === false),
todoListError: isError,
currentHighlightTodo: detailQuery.data.todo,
// ... more
};
}
重要数据提前加载
const prefetchTodos = async () => {
// The results of this query will be cached like a normal query
await queryClient.prefetchQuery('todos', fetchTodos)
}
或者手动灌入数据:
queryClient.setQueryData('todos', todos)
从 Mutation 响应更新列表
:::success ✅ 可以立刻生效的列表操作 BP,兼顾体验 + 性能,表达层面也非常简单。 :::
https://react-query.tanstack.com/guides/optimistic-updates
const queryClient = useQueryClient()
useMutation(updateTodo, {
// When mutate is called:
onMutate: async newTodo => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries('todos')
// Snapshot the previous value
const previousTodos = queryClient.getQueryData('todos')
// Optimistically update to the new value
queryClient.setQueryData('todos', old => [...old, newTodo])
// Return a context object with the snapshotted value
return { previousTodos }
},
// If the mutation fails, use the context returned from onMutate to roll back
onError: (err, newTodo, context) => {
queryClient.setQueryData('todos', context.previousTodos)
},
// Always refetch after error or success:
onSettled: () => {
queryClient.invalidateQueries('todos')
},
})
使用 factory 函数批量创建 Query 和 Mutation
通过创建简单的工厂函数,我们可以快捷创建用于请求的 query 和 mutation。
export type QueryRequestFunction<T, P> = (params: P) => Promise<T>;
export type GetQueryOptionsFn<Error, Result> = () => UseQueryOptions<unknown, Error, Result>;
/**
* useQuery 便捷工厂函数
* @description
* 用于快速生成 ReactQuery ReactBizHook
* 只需要包装一个 AxiosRequest
*/
export function useQueryFactory<Params, Result>(
/**
* 返回 AxiosResponse 对象的请求函数
*/
queryFunction: QueryRequestFunction<AxiosResponse<IContentResponse<Result>>, Params>,
/**
* query 名称,请确保唯一性
*/
queryName: string,
/**
* 初始化 queryOptions
* - 可以是一个对象
* - 可以是一个动态生成 queryOptions 的函数
*/
options?: UseQueryOptions<unknown, Error, Result> | GetQueryOptionsFn<Error, Result>,
/**
* 初始化 queryKey
*/
queryKey?: QueryKey,
) {
return function (params: Params, currentQueryOptions?: UseQueryOptions<unknown, Error, Result>) {
if (!queryName) {
const msg = 'useQueryFactory error: queryName is not a string, please offer a unique identifier for each query';
dtLogger.error(`[useQueryFactory] error: ${msg}`);
throw new Error(msg);
}
const presetOptions = typeof options === 'function' ? options() : options;
const q = useQuery(
queryKey || [queryName || 'query.anonymous', params],
async () => {
return handleResponse(await queryFunction(params));
},
{ ...presetOptions, ...currentQueryOptions },
);
return q as UseQueryResult<Result, Error>;
};
}
高阶用法
- Presist Mutations:mutations 可以存储在本地,延后激活消费
- QueryFilters:在部分批量操作 query 或者 mutation 的函数上自带的 filter 过滤项
// Cancel all queries
await queryClient.cancelQueries()
// Remove all inactive queries that begin with `posts` in the key
queryClient.removeQueries('posts', { inactive: true })
// Refetch all active queries
await queryClient.refetchQueries({ active: true })
// Refetch all active queries that begin with `posts` in the key
await queryClient.refetchQueries('posts', { active: true })
- SSR & Next.js 服务端渲染 React 使用
扩展插件
本地缓存 persistQueryClient
:::warning
✅ 本地持久化存储的好用法,前者直接使用 window.localStorage
或者 window.sessionStorage
,后者可以通过实现特定接口,来调用本地存储服务,比如:
- 客户端提供的持久化存储接口(ClientStorage)
- IndexedDB
- … :::
broadcastQueryClient
broadcastQueryClient
is a utility for broadcasting and syncing the state of your queryClient between browser tabs/windows with the same origin.
:::warning ✅ 可以使用它来同步同源不同浏览器 Tab 的请求状态(这个对于 IDE 场景或者需要多屏、多 Tab 工作的场景是非常舒服的)。 :::
一些思考
待探索的
- ReactQuery 是如何做缓存系统的?基于简单 Hash 序列的算法是如何实现的?
- ReactQuery 的 Query 是否可以绘制一张「状态机」图来展示 Query / Mutation 的状态变化示意图?
- WebWorker 是否也可以和 ReactQuery 很好地运作,包括类似
broadcastQueryClient
API?
小结
网络请求,或者说,服务端状态在客户端的千丝万缕的关系维护,react-query
给出了自己的 BP,整体来说,是一个非常优秀的管理 React 客户端 App 状态的库。它里面的 BP(最佳实践)给使用者「开箱即用」,或者说「开箱即美好」的感觉,确实可以提升代码的简洁性,提升稳定性。