React Query 实践
本文正在参加「金石计划 . 瓜分6万现金大奖」
本文翻译自 TkDodo 的 Practical React Query
当 GraphQL
和 Apollo Client 在 2018 年流行起来的时候 (特别是 Apollo Client),有很多它是否完全取代了 Redux 和和 Redux 是否已经死了的声音。
我清楚的记得我当时并不明白这是怎么回事,为什么一些数据请求库,会取代你的全局状态管理库,它们之间有什么关系?
我印象中像 Apollo
这样的 GraphQL client
只会为你像 Axios 那样为你获取数据,然后你仍然需要使用某种方式来让你的应用程序访问这些数据
我错得不能再错了。
客户端数据 vs. 服务端数据
Apollo
给你的不仅是获取数据的能力,它还为这些服务端数据提供了缓存,这意味着你可以在多个不同的组件中调用一个 useQuery
,它只会请求一次,然后从缓存中返回
这听起来对我们来说似乎非常熟悉,但是是使用 Redux: 从服务端获取数据,并且可以在任何地方使用
似乎我们一直在把服务端数据当做客户端数据来使用,除了当你请求它时,你的应用并没有这些数据,只是展示了它的最新版本,真正的拥有者是服务端
对我来说,这引入了一个全新的数据思考范式,如果我们可以利用缓存来展示我们不曾拥有的服务端数据,那么真正的客户端状态就没剩下多少了,这让我明白了为什么很多人认为 Apollo
可以在很多情况下取代 Redux
React Query
我还没有使用过 GraphQL
,我们有一个现有的 REST API
,没有 over-fetching
的问题,很明显,我没有足够的痛点来进行转换,尤其是需要调整后端,这不是一个容易的事情
但是我仍然羡慕其获取数据的简单性,包括加载状态和错误状态的处理,要是 React 中也有类似的 REST API
就好了
进入 React Query
React Query
开源作者 Tanner Linsley 在 2019 年底开发的,它借鉴了 Apollo
中优秀的部分,并向其带入到了 REST API
,它适用于任何返回 Promise
的函数,并且采用 stale-while-revalidate
缓存策略略,它有合适的默认值,使数据尽可能的保持新鲜,同时也尽可能的向用户提早展示数据,除此之外,它还非常灵活,可以在默认值不够用的情况下自定义各种设置
不过这篇文章不会是对 React Query
的介绍
官方的文档是很好的解释,你还可以看各种视频,如果你想熟悉它,有一个React Query Essentials课程,你可以参加。
我更多想关注的是一些超越文档的实用技巧,对于已经在使用它的同学来说可能会很有用,这些都是我在过去几个月里学到的,当时我不仅在工作中积极使用这个库,而且还参与了 React Query
社区,在 Discord
和 GitHub Discussions
中回答问题
默认值的解释
我相信 React Query
的默认值选择的很好,但是它们有时可能会让你措手不及,特别是在你刚使用它的时候
首先: React Query
不会在每次渲染的时候调用 queryFn
即使默认 staleTime
为 0,你的程序可能在任何时候因为各种原因重新渲染,所以每次渲染的时候请求数据是疯狂的
如果你看到了一个预料之外的 refetch
,那可能是你刚刚聚焦了窗口,React Query
做了一个 refetchOnWindowFocus
的功能,这是一个很棒的生产功能,如果用户转到了一个不同的浏览器 Tab,然后又回到了你的应用程序,后台的重新请求会被自动触发,如果在用户离开的期间,服务端数据有什么变化,它们会被显示在用户的屏幕上,并且没有加载提示,如果重新请求的数据与缓存中的相同,那么你的组件将不会重新渲染
在开发期间,这可能会被频繁的触发,特别是在浏览器的 devtools 和你的应用程序之间的焦点切换也会触发它,
此外,cacheTime
和 staleTime
可能很容易混淆,所以我试着解释一下
-
StaleTime:查询的新鲜时间,只要查询是新鲜的,数据会始终从缓存中返回,并且不会发送网络请求,如果查询过期(默认是 0 即立即过期),你仍然会从缓存中获取数据,但是一个 refetch 可能在后台发生
-
CacheTime:从缓存中删除非活跃的查询之前的持续时间,默认是五分钟,一旦没有注册观察者,查询就会过渡到非活跃的状态,即当一个查询的组件都卸载时
使用 React Query DevTools
这会帮助你了解查询所处的状态,以及当前缓存中的数据,这会方便你的调试,此外如果想要识别后台 refetch
,可以在浏览器的 devtools
中限制你的网络连接,因为开发服务器通常非常快
把 query key 当做依赖数组
我这里指得是 useEffect
钩子的依赖数组,我想你对它很熟悉
为什么它们是相似的
因为每当 query key
发生变化时,React Query
都会触发一个 refetch
,因此,如果我们传递给 queryFn
一个 params
参数时,当 params
发生变化时我们大概率想要重新获取数据,所以与其利用复杂的手段来触发 refetch
,我们不如利用 query key
type State = 'all' | 'open' | 'done'
type Todo = {
id: number
state: State
}
type Todos = ReadonlyArray<Todo>
const fetchTodos = async (state: State): Promise<Todos> => {
const response = await axios.get(`todos/${state}`)
return response.data
}
export const useTodosQuery = (state: State) =>
useQuery(['todos', state], () => fetchTodos(state))
如上代码,想象一下我们的 UI 显示一个 todo 列表和一个筛选项,我们会有一个本地的状态来存储这些筛选项,一旦用户改变了他们的选择,我们就会更新这个状态,而 React Query
会触发 refetch
因为 query key
变化了,因为我们也必须将 params
变量与 queryFn
同步,这与 useEffect
的依赖数组非常相似,我觉得我从来没有传递给 queryFn
一个没有作为 query key
的变量
一条新的缓存记录
因为 query key
是作为缓存的 key,所以当你第一次从 all
切换到 done
的时候,你会获得一条新的缓存记录,这将导致一个硬加载的状态 (可能会展示一个 loading spinner),这是不理想的,所以在这些情况下你可以使用用 keepPreviousData
选项,或者如果可能,你可以使用 initialData
预填充新创建的缓存,上面的例子就很完美,我们可以对我们的数据做一些客户端的预过滤
type State = 'all' | 'open' | 'done'
type Todo = {
id: number
state: State
}
type Todos = ReadonlyArray<Todo>
const fetchTodos = async (state: State): Promise<Todos> => {
const response = await axios.get(`todos/${state}`)
return response.data
}
export const useTodosQuery = (state: State) =>
useQuery(['todos', state], () => fetchTodos(state), {
initialData: () => {
const allTodos = queryClient.getQueryData<Todos>(['todos', 'all'])
const filteredData =
allTodos?.filter((todo) => todo.state === state) ?? []
return filteredData.length > 0 ? filteredData : undefined
},
})
现在每次用户在切换状态时,如果缓存中没有数据,我们会先使用 todos all
中的数据进行预填充,一旦后台请求完成,用户会看到新的列表,注意在 v3 之前还需要设置 initialStale
属性,来实际触发后台请求
我觉得这是一个很棒的用户体验改进,只需几行代码
保持服务端和客户端的状态分离
这与我上个月写的一篇文章 puting-props-to-use-state 是相辅相成的,如果你使用 useQuery
获取数据,尽量不要把这些数据放进本地 state
,主要原因是 React Query
会为你做所有的后台更新,但是"副本"不会
如果你想在数据获取后渲染你的表单,并为你的表单设置默认值,如下例子,这是没问题的,但是请确保设置 staleTime
为 Infinity
来避免不必要的后台 refetch
const App = () => {
const { data } = useQuery('key', queryFn, { staleTime: Infinity })
return data ? <MyForm initialData={data} /> : null
}
const MyForm = ({ initialData} ) => {
const [data, setData] = React.useState(initialData)
...
}
但是当你显示的需要用户可以编辑时,这个理念更难贯彻一些,我准备了一个例子,它有很多优点
这个演示最重要的一点是我们没有把从 React Query
拿到的数据放在本地,这确保了我们看到的始终是最新的值,
enabled 选项非常强大
useQuery
有许多选项可以自定义它的行为,enable
选项可以非常强大,可以让你做很多酷的事情,下边是使用这个选项能实现的一些功能:
-
只有第一个查询请求成功之后才进行第二个查询
-
停启用查询
由于
React Query
支持轮询数据(refetchInterval
),但是如果 Modal 打开,我们想要它停止 -
等待用户输出
query key
中有一些筛选条件,但是如果用户没有输入就可以禁用它 -
用户输入之后禁用一些查询
例如我们有一个临时值,优先于服务器数据,参考上面的例子
不要把查询缓存当做本地的状态管理器
如果你更改 queryCache(queryClient.setQueryData)
,它应该仅限于乐观更新或者一个突变(useMutation)后从服务端收到的数据,每一次后台的 refetch 都有可能覆盖这些数据,所以本地状态需要使用其它方式存储
创建一个自定义的钩子
即使只为 useQuery
简单的包一层,也可以获得如下好处:
-
可以将实际请求数据的
queryFn
和 UI 分离,但是和useQuery
放在同一个地方 -
可以将
query key
和它们的用法放在同一个文件里(包裹潜在的类型定义) -
如果需要增加或者调整一些数据转换,可以在一个文件夹内完成
你已经在上边的例子 (todos) 看到了这种用法
本文正在参加「金石计划 . 瓜分6万现金大奖」
转载自:https://juejin.cn/post/7164602747361165349