前情提要
在看 context API 相關的文件時,發現了一篇 React repo 中的討論,主題是 useContext 如何避免非必要的重複 render,React team 提供的解答會使用 memo()、useMemo() 等方法。因為 memo() 在我的記憶裡,只會追蹤 props 的改變,所以更加好奇到底可以怎麼實作?
以下是我的閱讀筆記,並且會補充、簡介 React 中 memoization 的 API,希望可以幫助到各位。討論原文可以在這裏 https://github.com/facebook/react/issues/15156 看到。
Memoization 的方法很多
React 有很多 memoization 的方法可以使用,class component 可以使用 shouldComponentUpdate, React.memo()
則是可以使用在 function component 裡頭,此外,還有一支 Hook API 叫做 useMemo()。
React.memo 跟 useMemo 又有什麼區別?React.memo() 是一個 HOC,可以用來包住元件,根據 props 的變更決定 render 與否;而 useMemo 是用來在一個元件中、包住函示的,我們可以使用它來確保 function 內的 value 只有在 dependencies 變動時,才會被重新運算(re-computed)。
仔細介紹文章中的 React.memo
const MyComponent = React.memo(function MyComponent(props) {
/* 這裡寫你的元件 */
});
當你的元件一直接收到相同的 props 因而不斷 render 出同樣的結果時,為了提升效能、減少不必要的 render,你可以使用 React.memo 把你的元件包在裡面,如果 props 沒有改變, React 會略過 re-render 你的元件,並且複用前一次 render 的結果。
但如果你的 component 含有 useState、useReducer、useContext 這類的 hook,當 state 或者 context 改變的時候,元件依然會被 re-render,因為 memo 只比較 props
另外,memo 只會 shallow 的比較你的 props 變更,你也可以自製這個比較差異的函示,把它當成第二個參數傳入即可。
function MyComponent(props) {
/* 元件內容 */
}
function areEqual(prevProps, nextProps) {
/*
用來比較差異的函示
*/
}
export default React.memo(MyComponent, areEqual);
/// 注意這裡的 areEqual 是 return true or false
讓我們看一個情境
一位工程師同仁發布了一個 report,以下是他在嘗試比對 context 以節省效能的過程中遇到的疑問:
// “我應該在 memo() 的第二個參數 callback 裡使用 context & nextProps 做差異比較,
// 還是有其他提升 useContext 效能的做法?”
React.memo(() => {
let globalState = useContext(AppContext);
// render something
}, (prevProps, nextProps) => {
// do comparison
});
你會怎麼做呢?討論中提到了三個作法:
最理想的做法是:假設被用在元件裡的 value 是 globalState.theme,而 globalState.theme 也被許多元件所需要,但 globalState 的變更太頻繁,會導致有使用 globalState.theme 也一起頻繁 render,此時我們可以把 globalState.theme 拆出來,變成一支獨立的 context,這是變更最小的方法。
function Button() {
// 這裏把 theme 拆成獨立的 ThemeContext
let theme = useContext(ThemeContext);
return <ExpensiveTree className={theme} />;
}
第二種方法是把元件拆分成兩個,在中間放上 memo():
function Button() {
let globalState = useContext(AppContext);
// 傳送 globalState.theme 這個更特定的 context 到下層,降低 re-render 機會
let theme = globalState.theme;
return <ThemedButton theme={theme} />
}
const ThemedButton = memo(({ theme }) => {
// 其餘的 render 邏輯放在這
// 這一層會被 memo 住,藉此節省效能
return <ClickArea color={theme} />;
});
如果你無法使用第一種方法把 context 拆開,你可以試試看把元件拆開,然後傳送更特定的 props 給最下層的那個元件(<ThemedButton>
)。你的外層元件(<Button>
)還是會容易被 re-render,但下層元件就可以被 keep 住,這樣一來,應該可以挽救很多效能(因為外面那一層也沒有做什麼事情)。
第三種方法是使用 useMemo:
最後一種方法,是把 return component 包在 useMemo 裡,只要 useMemo dependencies 不改變 React 就不會 re-render,跟第二種方法相似。
function Button() {
let globalState = useContext(AppContext);
let theme = globalState.theme;
return useMemo(() => {
// 其餘的 render 邏輯放在這
// 這一層會被 memo 住,藉此節省效能
return <ThemedButton theme={theme} />;
}, [theme])
}
最後,底下的討論也提到,如果你的 APP 內部結構複雜龐大、要共用的資訊多,那可以考慮改用 Redux 來管理狀態。