阅读 51

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 设置为 trackedReact 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


文章分类
代码人生
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐