本文正在参加「金石计划 . 瓜分6万现金大奖」
免责声明:渲染优化对于任何应用来说都是高级概念,React Query
已经有了很棒的开箱即用的优化,并且在大部分情况下,不需要进一步优化,不需要 re-render
是很多人关注的主题,这也是我想要谈论它的原因,但是我想再次指出,大多数情况下,对于大多数应用,渲染优化可能并不像你觉得那么重要,re-render 它确保了你的应用是最新的,我每天都会选择一个不必要的渲染
,而不是丢失一个需要的渲染
,更多请参考:
-
Fix the slow render before you fix the re-render by Kent C. Dodds
在 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
来关闭
如果你想要了解更多引擎盖下发生了什么,可以参考这些 replaceEqualDeep tests
本文正在参加「金石计划 . 瓜分6万现金大奖」