React Query 渲染优化
免责声明:渲染优化对于任何应用来说都是高级概念,React Query
已经有了很棒的开箱即用的优化,并且在大部分情况下,不需要进一步优化,不需要 re-render
是很多人关注的主题,这也是我想要谈论它的原因,但是我想再次指出,大多数情况下,对于大多数应用,渲染优化可能并不像你觉得那么重要,re-render 它确保了你的应用是最新的,我每天都会选择一个不必要的渲染
,而不是丢失一个需要的渲染
,更多请参考:
Fix the slow render before you fix the re-render by Kent C. Dodds
this article by @ryanflorence about premature optimizations
在 select
选项一章,对于渲染优化,我已经写了不少内容,但是"为什么 React Query 重新渲染了两次,即使我的数据没有变化"可能是我最需要回答的问题,因此,让我来试着深入解释一下
isFetching
在上一个例子中,我说这个组件只会在 length
变化时重新渲染,这其实不完全正确
export const useTodosQuery = (select) => useQuery(['todos'], fetchTodos, { select }) export const useTodosCount = () => useTodosQuery((data) => data.length) function TodosCount() { const todosCount = useTodosCount() return <div>{todosCount.data}</div> } 复制代码
每当进行一个后台重新请求,这个组件都会重新渲染两次,并输出如下的 queryInfo
{ status: 'success', data: 2, isFetching: true } { status: 'success', data: 2, isFetching: false } 复制代码
这是因为 React Query
对每个 query 都暴露出去了很多元数据,isFetching
正是其中一个,它会在每次请求的过程中变为 true
,这在你想展示一个 loading
的时候非常有用,但是如果你不需要,那么它也就没必要了
notifyOnChangeProps
对于这种情况,React Query
有一个 notifyOnChangeProps
选项,它可以在每个观察者层面设置,用来告诉 React Query
:请只有在这些数据发生变化时,才通知当前观察者,通过设置 ['data']
,我们将找到我们所寻求的优化版本
export const useTodosQuery = (select, notifyOnChangeProps) => useQuery(['todos'], fetchTodos, { select, notifyOnChangeProps }) export const useTodosCount = () => useTodosQuery((data) => data.length, ['data']) 复制代码
你可以在文档的 optimistic-updates-typescript 找到这个功能
保持同步
虽然上边的代码可以工作,但是它也很容易不同步,如果我们想处理错误呢,或者我们开始使用 isLoading
,我们必须将我们在组件中使用的字段添加到 notifyOnChangeProps
选项中,如果我们忘记了,只监听了 data
,那么当错误发生时,我们的组件就不会重新渲染,并且展示过时的数据,或者我们可以在自定义钩子中硬编码,这就比较麻烦,因为钩子不知道组件使用什么
export const useTodosCount = () => useTodosQuery((data) => data.length, ['data']) function TodosCount() { // ???? we are using error, but we are not getting notified if error changes! const { error, data } = useTodosCount() return ( <div> {error ? error : null} {data ? data : null} </div> ) } 复制代码
正如一开始说的,我认为这比偶尔的不需要的重新渲染更加糟糕,当然,我们可以把这个选项从组件中传递给自定义钩子,但是这仍然让人觉得很费劲,有没有一种办法可以自动做到这一点,事实上,是有的
Tracked Queries
我为这个功能相当自豪,因为这是我对它的第一个重大贡献,如果你把 notifyOnChangeProps
设置为 tracked
,React Query
会追踪你所使用的值,并将计算为一个 list
,这与你手动传递没有区别,只是你不必再考虑这个问题了,当然你也可以全局打开这个选项
const queryClient = new QueryClient({ defaultOptions: { queries: { notifyOnChangeProps: 'tracked', }, }, }) function App() { return ( <QueryClientProvider client={queryClient}> <Example /> </QueryClientProvider> ) } 复制代码
这样,你就不用再担心重新渲染的问题了,当然追踪是有一定的开销的,所以确保你明智的使用它,此外还存在一些限制,这也是为什么这是一个可选的功能
如果你使用 Object Rest Destructuring,它将观察所有字段
// ???? will track all fields const { isLoading, ...queryInfo } = useQuery(...) // ✅ this is totally fine const { isLoading, data } = useQuery(...) 复制代码
追踪只发生在渲染期间,所以如果你只在副作用内访问字段,那么它不会被追踪
const queryInfo = useQuery(...) // ???? will not corectly track data React.useEffect(() => { console.log(queryInfo.data) }) // ✅ fine because the dependency array is accessed during render React.useEffect(() => { console.log(queryInfo.data) }, [queryInfo.data]) 复制代码
追踪的字段不会在每次渲染的时候重置,所以,如果你追踪了一个字段一次,那么在观察者的整个生命周期内,你都会追踪它
const queryInfo = useQuery(...) if (someCondition()) { // ???? we will track the data field if someCondition was true in any previous render cycle return <div>{queryInfo.data}</div> } 复制代码
更新:从 v4
开始,追踪查询是默认值,你可以使用notifyOnChangeProps: 'all'
关闭它
结构共享
结构共享是 React Query
另一个非常重要的,开箱即用的优化,这个特性确保我们的数据在每一层级上的引用都是稳定的,假设我们有以下的数据结构
[ { "id": 1, "name": "Learn React", "status": "active" }, { "id": 2, "name": "Learn React Query", "status": "todo" } ] 复制代码
现在我们把第一条的数据从 active
状态过度到 todo
,并且在后台进行了 re-fetch
,我们将从后端获得一个全新的 json,
[ - { "id": 1, "name": "Learn React", "status": "active" }, + { "id": 1, "name": "Learn React", "status": "done" }, { "id": 2, "name": "Learn React Query", "status": "todo" } ] 复制代码
现在 React Query
会把旧的数据和新的数据进行比较,以保证保留更多的数据,在我们的例子中,整个数组会是新的,因为我们更新了一条数据,id 为 1 的对象会是新的,但是 id 为 2 的对象会保持和旧数据相同的引用,React Query
会将它赋值到新的数据中,因为它没有任何变化
当我们使用选择器进行部分订阅时,这就非常方便了
// ✅ will only re-render if _something_ within todo with id:2 changes // thanks to structural sharing const { data } = useTodo(2) 复制代码
正如我之前暗示的那样,对于 selector
来说,结构共享进行两次,一次是在 queryFn
返回结果时,用来确定是否有任何东西改变 ,一次是应用于 selector
的返回值,在一些情况下,特别是有巨大的数据时,结构共享可能是一个瓶颈,同时它也仅适用于可序列化的数据,如果你不需要这个优化,你可以通过 structuralSharing: false
来关闭
作者:度123
链接:https://juejin.cn/post/7169141418454155295