五个避免 React 组件重渲染的方法
原文作者:Chameera Dulanga
React 组件从开始到现在已经发展了很长一段时间。虽然如此,许多开发者发现还是很难去减少不必要的重渲染。但实际上是有很多方法去避免这个问题的。
在这篇文档,我会讨论 5 个方法去避免 React 组件中不必要的重渲染。
使用 useMemo() 和 useCallback() 钩子记忆
这个记忆可以使得组件仅在 props
数据发生变化时才进行重渲染。通过这个技巧,开发者可以避免无用的渲染并减少应用程序中的计算负载。
react 提供了两个钩子去实现记忆:
useMemo()
useCallback()
在输入相同的情况下,无需重新计算这些钩子,通过缓存并返回相同的结果来减少重渲染。当输入有变化时,缓存就会失效并渲染新的组件状态。
useMemo()
为了更好的理解如何使用 useMemo()
钩子,让我们来看下两数相乘的一个例子:
const multiply = (x,y) => {
return x*y
}
上面的这个方法不管输入是什么,每次被调用时都会计算结果并重新渲染组件。但是,如果我们使用了 useMemo()
钩子,当输入是相同的情况下且结果被保存在缓存中时,我们可以避免组件的重新渲染。
const cachedValue = useMemo(() => multiply(x, y), [x, y])
现在,计算的结果会被保存在 cachedValue
变量中,并且除非输入发生了变化 useMemo()
钩子每次都会返回这个结果。
useCallback()
useCallback()
是另一个实现记忆的 react 钩子。但是,和 useMemo()
不同,这个钩子不缓存结果。相反,它记住的是所提供给它的回调函数。
例如,考虑一个具有可点击项目列表的组件。
import { useCallback } from 'react';
export function MyParent({ item }) {
const onClick = useCallback(event => {
console.log('Clicked Item : ', event.currentTarget);
}, [item]);
return (
<Listitem={item} onClick={onClick}
/>
);
}
在上面这个例子中,useCallback()
记住了 onClick
的回调。因此,当用户重复点击相同的项目时,不会导致组件的重新渲染。
使用 React Query 来优化 API 调用
在 React 应用中我们经常会使用 useEffect()
钩子用来做异步获取数据的操作。但是,在每次渲染时useEffect()
都会执行获取数据,并且在大多数情况下,它一直加载相同的数据。
作为解决方案,我们可以使用 React Query 库去缓存响应的数据。当我们调用 API 时,React Query 会在请求之前先从缓存中返回数据。然后,它会从服务端检索数据,如果发现没有可用的新数据,它将阻止组件的重渲染。
import React from 'react'
import {useQuery} from 'react-query'
import axios from 'axios'
async function fetchArticles(){
const {data} = await axios.get(URL)
return data
}
function Articles(){
const {data, error, isError, isLoading } = useQuery('articles', fetchArticles)
if(isLoading){
return <div>Loading...</div>
}
if(isError){
return <div>Error! {error.message}</div>
}
return(
<div>
...
</div>
)
}
export default Articles
React Query 库目前在 NPM 上的每周下载量超过 60 万次,并且在 Githhub 上有 1.3k+ 的 star。
使用 Reselect 创建有记忆能力的 Selector
Reselect 是一个用于创建有记忆能力 selector 的三方包。它通常会与 Redux 的 store 搭配使用,并且它能很好的减少不必要的重渲染。
- Reselect selectors 能够计算派生数据
- Reselect selectors 不会重新计算除非传入的参数变化了
- 它们可以作为其他 selector 的输入
Reselect 提供一个名叫 createSelector
的 api,这个 api 可以生成具有记忆能力的函数。为了更好的理解,可以看下面给出的这个例子。
import { createSelector } from 'reselect'
...
const selectValue = createSelector(
state => state.values.value1,
state => state.values.value2,
(value1, value2) => value1 + value2
)
...
在这个例子中 createSelector
将两个 selector 作为输入,然后返回一个具有记忆能力的版本。在更改值之前,不会使用这个记忆版本再次计算 selector。
Reselect 库目前在 NPM 上的每周下载量超过两百万次,并且在 Githhub 上有 18.4k+ 的 star。
使用 useRef() 代替 useState()
在 React 应用中useState()
钩子被广泛的用于当状态发生变化时重渲染组件。但是,在一些场景中我们需要在不重新渲染组件的情况下,跟踪状态的变化。
但是,如果我们使用 useState()
钩子,我们不能达到跟踪状态变化而不触发组件的重渲染。
function App() {
const [toggle, setToggle] = React.useState(false)
const counter = React.useRef(0)
console.log(counter.current++)
return (
<button onClick={() => setToggle(toggle => !toggle)} >
Click
</button>
)
}
ReactDOM.render(<React.StrictMode><App /></React.StrictMode>, document.getElementById('mydiv'))
在上面这个例子中,每当值发生变化时,都会导致组件的重渲染。但是 counter
计算器保留了它的值,因为它是一个可变的标识。由于我们使用的是 useRef()
,它只会引起单个渲染。但是,如果我们使用的是 useState()
,在每次切换时它都会导致两次渲染。
使用 React Fragments
如果你以前使用过 React,你就会知道 React 需要在组件外用单个父元素包裹组件。虽然这并不会导致重渲染,但你知道这会影响到整体组件的渲染时间吗?
作为解决方案,可以使用 React Fragment 来包裹组件,它会减少 DOM 的负载,进而加快渲染时间,并减少内存的使用。
const App= () => {
return (
<React.Fragment><p>Hello<p/><p>World<p/></React.Fragment>
);
};
总结
在这篇文章中,我讨论了 5 个不同的方法用于在 React 组件中阻止不必要的重渲染。大多数解决方案都是通过缓存,你也可以使用内置的 React 钩子或者三方库来实现它们。
此外,这些方法也能提升你的应用性能,以防止不必要的重渲染,同时减少内存开销。