免责声明:渲染优化是任何应用程序的高级概念。 React Query 已经提供了非常好的优化和开箱即用的默认设置,并且大多数情况下,不需要进一步的优化。 “不需要的重新渲染”是许多人倾向于关注的话题,这就是我决定涵盖它的原因。但我想再次指出,通常,对于大多数应用程序,渲染优化可能并不像您想象的那么重要。重新渲染是一件好事。他们确保您的应用程序是最新的。我每天都会对“应该存在的缺失渲染”进行“不必要的重新渲染”。有关此主题的更多信息,请阅读:
在描述 「第二部分:数据转换」 中的选择选项时,我已经写了很多关于渲染优化的文章。然而,“为什么 React Query 重新渲染我的组件两次,即使我的数据没有任何变化”是我可能最需要回答的问题(除了可能:“我在哪里可以找到 v2 文档”😅)。所以让我试着深入解释一下。
isFetching 过渡
在上一个例子中,当我说这个组件只会在 todos 的长度改变时重新渲染时,我并不是完全诚实的:
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>
}
每次进行后台重新获取时,此组件将使用以下查询信息重新渲染两次:
{ status: 'success', data: 2, isFetching: true }
{ status: 'success', data: 2, isFetching: false }
那是因为 React Query 为每个查询暴露了很多元信息, isFetching 就是其中之一。当请求正在进行中时,此标志将始终为真。如果您想显示后台 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'])
您可以在文档中的乐观更新打字稿示例中看到这一点。
保持同步
虽然上面的代码运行良好,但它很容易不同步。如果我们也想对错误做出反应怎么办?或者我们开始使用 isLoading 标志?我们必须使 notifyOnChangeProps 列表与我们在组件中实际使用的任何字段保持同步。如果我们忘记这样做,我们只观察 data 属性,但得到一个我们也显示的错误,我们的组件将不会重新渲染,因此已经过时了。如果我们在自定义钩子中对此进行硬编码,这将特别麻烦,因为钩子不知道组件实际使用什么:
export const useTodosCount = () =>
useTodosQuery((data) => data.length, ['data'])
function TodosCount() {
// 🚨 我们使用了 error,但当 error 改变时我们没有得到响应
const { error, data } = useTodosCount()
return (
<div>
{error ? error : null}
{data ? data : null}
</div>
)
}
正如我一开始在免责声明中所暗示的那样,我认为这比偶尔不必要的重新渲染要糟糕得多。当然,我们可以将选项传递给自定义钩子,但这仍然感觉非常繁琐。有没有办法自动执行此操作?其实是有的:
跟踪查询
我为这个功能感到非常自豪,因为这是我对库的第一个主要贡献。如果您将 notifyOnChangeProps 设置为 'tracked',React Query 将跟踪您在渲染期间使用的字段,并使用它来计算列表。这将与手动指定列表完全相同的方式进行优化,只是您不必考虑它。您还可以为所有查询全局启用此功能:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
notifyOnChangeProps: 'tracked',
},
},
})
function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
)
}
有了这个,你再也不用考虑重新渲染了。当然,跟踪使用情况也有一些开销,因此请确保明智地使用它。跟踪查询也有一些限制,这就是为什么这是一个选择加入的功能:
如果您使用对象静止解构,您将有效地观察所有字段。正常的解构没问题,只是不要这样做: 有问题的休息破坏
// 🚨 将跟踪使用属性
const { isLoading, ...queryInfo } = useQuery(...)
// ✅ 这就没问题
const { isLoading, data } = useQuery(...)
跟踪查询仅在“渲染期间”有效。如果您仅在效果期间访问字段,则不会跟踪它们。由于依赖数组,这是非常极端的情况:
const queryInfo = useQuery(...)
// 🚨 这将不会被跟踪
React.useEffect(() => {
console.log(queryInfo.data)
})
// ✅ 这是正确的做法,将依赖的字段访问一遍并置入依赖数组
React.useEffect(() => {
console.log(queryInfo.data)
}, [queryInfo.data])
跟踪查询不会在每次渲染时重置,因此如果您跟踪一个字段一次,您将在观察者的生命周期内跟踪它:
const queryInfo = useQuery(...)
if (someCondition()) {
// 如果 someCondition() 是 true,那么将一直追踪 queryInfo.data 的变化
return <div>{queryInfo.data}</div>
}
结构共享( structural sharing )
React Query 开箱即用的另一个同样重要的渲染优化是结构共享。此功能可确保我们在各个级别保留数据的引用。例如,假设您有以下数据结构:
[
{ "id": 1, "name": "Learn React", "status": "active" },
{ "id": 2, "name": "Learn React Query", "status": "todo" }
]
现在假设我们将第一个 todo 转换为 done 状态,并进行后台重新获取。我们将从后端获得一个全新的 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 将尝试比较旧状态和新状态,并尽可能多地保留以前的状态。在我们的示例中, todos 数组将是新的,因为我们更新了一个 todo 。 id 为 1 的对象也将是新的,但 id 为 2 的对象将与前一状态中的对象具有相同的引用,React Query 只会将其复制到新结果中,因为其中没有任何更改。
这在使用选择器进行部分订阅时非常方便:
// ✅ 只有当 id 为 2 的 todo 发生变化才能引起重渲染
// 多亏了 structural sharing
const { data } = useTodo(2)
正如我之前所暗示的,对于选择器,结构共享将进行两次:一次在从 queryFn 返回的结果上确定是否有任何更改,然后再一次在选择器函数的结果上。在某些情况下,尤其是在拥有非常大的数据集时,结构共享可能是一个瓶颈。它也只适用于可JSON系列化的数据。如果你不需要这个优化,你可以通过在任何查询上设置 structureSharing: false 来关闭它。
如果您想了解更多有关幕后发生的事情,请查看 replaceEqualDeep 测试。