::: tip
如果沒有特別說明,文中的組件都是指函數組件。
:::
基礎概念#
1. 應該如何看待函數式組件,他與類組件有什麼區別?#
函數式組件與類組件是完全不同的心智模型 (mental model)。
函數式組件是純函數,它們只是接受 props 並返回一個 React 元素。
類組件是一個類,它們有自己的狀態,生命週期,以及實例方法。
在實踐中,我們應該將函數式組件視作純函數,而類組件視作類。忘記生命週期那一套思維,千萬不要用各種操作在函數組件裡模擬生命週期。
一個純函數是沒有副作用的,而我們的應用必須要有副作用才有意義。因此 React 提供了 useEffect
來處理副作用。
2. 為什麼組件會重複渲染?#
這是 React 的一個特性,它會在每次 props 或 state 變化時重新渲染組件。以此來保證組件的狀態與視圖保持一致。
我們舉一個官網的例子:
組件是廚師, react 是服務員。
- 觸發 渲染(將客人的訂單送到廚房)
- 渲染 組件(在廚房準備訂單)
- 提交 到 DOM(將訂單放在桌子上)
當 props 或 state 變化時,React 會觸發重新渲染,也就是重新執行函數。
在最終的 Commit 階段,React 會依據函數的執行結果儘可能的重用 DOM 節點,以此來提高性能。
所以在 React 中,rerender 並不是一個 bug,而是一個特性。也不需要擔心性能問題,React 會自動優化。
建議配合官網文檔以及這篇文章一起看: Why React Re-Renders
如果還有疑問,建議再看幾遍參考資料:
- Render and Commit
- Why React Re-Renders
- 每隔一段時間再讀總有收穫
3. State 是什麼,為什麼需要它,為什麼有時候它的值與預期總是不一致?#
組件需要響應用戶的操作,而用戶的操作會導致組件的狀態發生變化。因此我們需要一個地方來存儲組件的狀態,這就是 state。
當 state 發生變化時,React 會重新渲染組件。 這就是 State 的運行機制。
同時這也是為什麼 state 的值與預期不一致的原因,因為每一次的重新渲染都是一次函數執行,在每次函數執行中,state 都有不同的值。所有這些渲染中,state 都是獨立的,互不影響。
下面是一個例子:
const Counter = () => {
const [count, setCount] = useState(0)
const onClick = () => {
setInterval(() => {
setCount(count + 1)
}, 1000)
}
return (
<div>
<p>Count: {count}</p>
<button onClick={onClick}>Click me</button>
</div>
)
}
點擊按鈕後,每隔一秒,計數器的值會增加 1。但是我們會發現,計數器的值會一直停留在 1。
我們可以看到,定時器只在第一次渲染函數的運行時裡,而這裡的 state 是 0,所以每一次定時器執行取到的 state 都是 0,那麼頁面上的值就會一直是 0 + 1 = 1。
我們可以將 state 理解為函數狀態快照,每次渲染都會有一份新快照,而這些快照是互不影響的。
4. useMemo 是什麼,我需要使用它嗎?#
useMemo
是一個 Hook,它可以用來緩存函數的返回值。
所以它唯一的用途就是提高性能,因為它可以避免重複計算。
因此,我們必須確保即使當去掉 useMemo
後,組件的行為也不會發生變化。
但是我們應該明確過早的優化是萬惡之源,所以在沒有性能問題的情況下,我們不應該使用 useMemo
。
這句話說的比較模糊,究竟什麼時候才是合適的時機呢?
答案是在絕大多數情況下,我們都不需要使用 useMemo
。
依據官方文檔的說法,我們可以使用如下代碼測試一個計算的耗時:
console.time('filter array');
const visibleTodos = filterTodos(todos, tab);
console.timeEnd('filter array');
// filter array: 0.15ms
當我們的計算耗時大於 1ms 時,我們就可以考慮使用 useMemo
來緩存計算結果了。
另外:useMemo
並不能優化第一次渲染的性能,它只能幫助我們在組件更新時避免重複計算。
既然 useMemo
可以優化性能,那麼為什麼不在每個地方都使用呢?
有三個原因:
useMemo
本身是有開銷的,它會在每次渲染時都執行,去比對依賴項是否發生變化,這個計算開銷有可能比我們要緩存的計算開銷還要大。(尤其還需要考慮數組、對象這種引用類型的依賴)useMemo
會使組件的行為變得不可預測,這會導致 bug 的產生。useMemo
會使組件的代碼變得難以理解,這會導致維護成本的增加。
我們之前說過,React 通過重複執行函數來實現組件的更新,而 useMemo
會跳過某些函數的執行,這就會導致組件的行為變得不可預測。維護者需要去理解這些跳過的函數,這會增加維護成本。
5. useCallback 是什麼,我需要使用它嗎?#
useCallback 和 useMemo 的作用是一樣的,都是用來緩存一些計算結果,但是它們的使用場景不同。
useCallback 用來緩存函數,而 useMemo 用來緩存值。
當一個函數或一個值作為組件的 props 傳遞給子組件時,如果這個函數或值沒有發生變化,那麼子組件就不會重新渲染。
所以很多人會使用 useCallback 來緩存函數,用 useMemo 來緩存值。
const TodoList = ({ todos, onClick }) => {
return (
<ul>
{/** ... */}
</ul>
)
}
const App = () => {
const todos = useMemo(() => filterTodos(todos, tab), [todos, tab])
const onClick = useCallback(() => {
// ...
}, [])
return (
<TodoList todos={todos} onClick={onClick} />
)
}
如上邊的例子,我們可以看到,我們使用了 useMemo 來緩存 todos,使用了 useCallback 來緩存 onClick。有些人會認為這優化了性能,因為我們避免了子組件的重新渲染。
但實際上這並沒有優化性能,因為只有當子組件是 memo 組件時,才會避免子組件的重新渲染。
const TodoList = React.memo(({ todos, onClick }) => {
return (
<ul>
{/** ... */}
</ul>
)
})
6. useEffect 是什麼,它有什麼用?#
useEffect 是一個 Hook,它可以用來處理副作用。
默認情況下它在每次組件渲染後執行,但可以接收一個依賴項數組,只有當依賴項發生變化時,才去執行。
useEffect
的設計目標並不是在函數組件中提供類似於生命週期的功能,而是用來處理副作用,也就是讓組件的狀態與外部世界同步。
我們看一個官網的例子:
const ChatRoom = ({ roomId }) => {
useEffect(() => {
const connection = createConnection(roomId) // 創建連接
connection.connect()
return () => {
connection.disconnect() // 斷開連接
}
}, [roomId])
}
// roomId 默認值 'general'
// 第一次操作 'general' 變為 'travel'
// 第二次操作 'travel' 變為 'music'
如果我們從組件的角度出發,它的行為是這樣的:
- 組件第一次渲染時,觸發 useEffect,連接到 'general' 房間
- roomId 變為 'travel',組件重新渲染,觸發 useEffect,斷開 'general' 房間的連接,連接到 'travel' 房間
- roomId 變為 'music',組件重新渲染,觸發 useEffect,斷開 'travel' 房間的連接,連接到 'music' 房間
- 組件卸載時,觸發 useEffect,斷開 'music' 房間的連接
看起來很完美,但是如果我們從 useEffect 的角度出發,它的行為是這樣的:
- Effect 連接到 'general' 房間,直到斷開連接
- Effect 連接到 'travel' 房間,直到斷開連接
- Effect 連接到 'music' 房間,直到斷開連接
當我們從組件的角度來看待 useEffect 時,useEffect 就變成了一種在組件渲染完成後或者卸載前執行的一種回調函數、生命週期。
而從 useEffect 的角度出發,我們只關心應用如何開始或終止與外部世界的同步。就像寫組件的 rendering 代碼一樣,接收 state,返回 js。我們不會考慮 rendering 代碼在 mount、update、unmount 時會發生什麼。我們只關注單次的渲染它應該是什麼樣的。
最後,我們來看有這樣一種說法:
The question is not "when does this effect run" the question is "with which state does this effect synchronize with"
useEffect(fn) // all state
useEffect(fn, []) // no state
useEffect(fn, [these, states])
Tweet not found
The embedded tweet could not be found…
重要的不是 useEffect 什麼時候執行,而是同步了哪些狀態。
狀態管理#
1. 什麼是狀態管理,為什麼它在 React 應用中很重要?#
狀態管理是指在一個應用程序中追蹤、更新和維護數據(狀態)的過程。在 React 應用中,狀態管理尤為重要,因為它直接影響到應用的用戶界面和交互。當狀態發生變化時,React 會自動更新相關的組件以反映這些更改。
在 React 應用中,狀態管理的重要性主要體現在以下幾個方面:
- 可預測性:良好的狀態管理可以使應用的行為更加可預測,開發人員可以更容易地追蹤和理解狀態變化的來源。
- 可維護性:通過組織和管理狀態,可以使代碼更易於維護,降低應用程序複雜性。這有助於團隊在項目中更高效地協作。
- 可擴展性:當應用程序變得越來越複雜,狀態管理可以幫助開發人員更好地組織代碼和邏輯,從而提高應用程序的可擴展性。
- 性能優化:有效地管理狀態可以減少不必要的組件重新渲染,從而提高應用程序的性能。
在 React 中,有多種狀態管理方法,例如使用組件內部狀態(如 useState Hook)、上下文(Context)API 以及第三方狀態管理庫(如 Redux、MobX 或 jotai 等)。
2. 如何在函數式組件中使用 useState Hook 管理狀態?#
useState
是 React 提供的一個內置 Hook,它允許在函數式組件中添加和更新狀態。
在函數式組件內部,調用 useState
函數,並傳遞初始狀態值作為參數。useState
會返回一個包含兩個元素的數組:當前狀態值和一個用於更新狀態的函數。通常,我們使用數組解構賦值來獲取這兩個值。
const [state, setState] = useState(initialState);
以下是一個簡單的示例
import React, { useState } from 'react';
const Counter = () => {
// 使用 useState Hook 初始化計數器狀態
const [count, setCount] = useState(0);
// 定義一個函數,用於增加計數器的值
const increment = () => {
setCount(_count => _count + 1);
};
return (
<div>
<p>Current count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
export default Counter;
在這個示例中,我們創建了一個簡單的計數器組件。我們使用 useState Hook 來存儲計數器的當前值,並定義了一個 increment 函數來更新計數器。當用戶點擊 "Increment" 按鈕時,計數器的值將遞增。
useState
是一個非常實用的 Hook,但在使用過程中可能會遇到一些易出錯的點。以下是一些需要注意的問題:
- 不要在條件語句中使用 useState:React 依賴於 Hook 調用的順序一致來確保正確關聯和管理狀態和副作用,因此,請確保每次渲染時都以相同的順序調用 Hook。不要在循環、條件語句或嵌套函數中調用 Hook。
// 錯誤示範
if (condition) {
const [state, setState] = useState(initialState);
}
- 異步更新:
setState
函數是異步的。這意味著當你調用setState
時,狀態更新可能不會立即生效。如果你需要根據當前狀態計算新狀態,請使用setState
函數的函數式更新形式。
// 正確示範
const increment = () => {
setCount((prevCount) => prevCount + 1);
};
- 在更新時合併狀態:與類組件中的
setState
不同,函數式組件中的useState
在更新狀態時不會自動合併對象。如果你的狀態是一個對象,請確保在更新時手動合併狀態。
const [state, setState] = useState({ key1: 'value1', key2: 'value2' });
// 錯誤示範
setState({ key1: 'new-value1' }); // 這會導致 key2 丟失
// 正確示範
setState((prevState) => ({ ...prevState, key1: 'new-value1' }));
- 初始化時避免重複計算:如果你的初始狀態需要通過複雜計算或副作用函數來獲取,可以將初始狀態計算函數傳遞給
useState
,以避免在每次渲染時都進行計算。
const [state, setState] = useState(() => computeExpensiveInitialState());
- 初始值只會在組件首次渲染時使用:之後的重新渲染將保持和使用已經設置的狀態值,而不會重新應用 initialState。
因此,在使用 useState 時,需要確保正確理解這一行為。如果你需要根據屬性(props)或其他外部變量來設置狀態的初始值,請確保在狀態更新邏輯中正確處理這些依賴關係。
第一種方案是使用 key
屬性來觸發組件的重新渲染,只需在使用組件時將 key
屬性設置為一個唯一值。當需要根據屬性(如initialCount
)重新渲染組件時,可以將 key
設置為該屬性值:
import React from 'react';
import MyComponent from './MyComponent';
function ParentComponent() {
const [initialCount, setInitialCount] = useState(0)
return <MyComponent key={initialCount} initialCount={initialCount} />;
}
export default ParentComponent;
在這個示例中,當 initialCount
屬性發生變化時,MyComponent
組件將使用新的 key 值進行重新渲染。這將導致組件根據新的 initialCount
值進行初始化和掛載。
第二種方案是使用 useEffect
Hook 來處理外部變量的變化,從而根據需要更新組件狀態。
import React, { useState, useEffect } from 'react';
function MyComponent({ initialCount }) {
const [count, setCount] = useState(initialCount);
useEffect(() => {
// 當 initialCount 屬性值發生變化時,更新組件狀態
setCount(initialCount);
}, [initialCount]);
}
export default MyComponent;
使用 key
方案優點是心智負擔小,組件的狀態更清晰可預測,缺點是由於 key
的變更會導致整個組件的卸載和掛在,可能會帶來較高的性能開銷。
使用 useEffect
方案的優點是僅在屬性值發生變化時觸發重新渲染,而不需要卸載和掛載整個組件。性能開銷更低。缺點是需要手動管理可能存在的副作用的清除和重新應用,需要更多的代碼來處理屬性值的變化和狀態更新,組件的狀態更加複雜。
根據具體需求和性能要求,可以在這兩種方案之間進行權衡。就我個人來說,性能是不需要過早考慮的問題,相反代碼的可維護性,狀態的可預測性可能對項目質量的影響更大,所以在大部分的場景下我會優先推薦 key
的方案。
3. 什麼是上下文(Context)API,它如何解決狀態共享問題?#
上下文(Context)API 是 React 中一種用於在組件樹中共享狀態的方法,無需顯式地通過屬性(props)逐層傳遞。它允許你在組件樹的某個層級設置一個值,然後在較低層級的任何組件中直接訪問該值。這在管理跨越多個層級的共享狀態時非常有用,避免了逐層傳遞屬性的繁瑣。
要使用 Context API,需要執行以下步驟:
- 創建一個上下文對象:使用
React.createContext
函數創建一個新的上下文對象。此函數接受一個默認值作為參數,該值將在未找到匹配的上下文提供者(Provider)時使用。
const MyContext = React.createContext(defaultValue);
- 添加上下文提供者(Provider):在組件樹中的適當位置添加上下文提供者。提供者接受一個 value 屬性,該屬性將作為上下文值傳遞給消費者(Consumer)。
<MyContext.Provider value={/* shared value */}>
{/* children components */}
</MyContext.Provider>
- 在子組件中使用上下文:在組件樹的任何較低層級中,可以使用
useContext
Hook 或上下文消費者(Consumer)組件來訪問上下文值。
// 使用 useContext Hook
import React, { useContext } from 'react';
function MyComponent() {
const contextValue = useContext(MyContext);
// ...
}
// 使用 Context.Consumer 組件
import React from 'react';
function MyComponent() {
return (
<MyContext.Consumer>
{contextValue => {
// ...
}}
</MyContext.Consumer>
);
}
通過使用 Context API,你可以在組件樹的任何位置輕鬆共享狀態,無需逐層傳遞屬性。這使得跨多個層級的組件之間的狀態共享變得更加簡潔和高效。然而,需要注意的是,過度使用上下文可能導致組件之間的耦合過於緊密,從而降低代碼的可維護性。因此,在使用 Context API 時,請確保在確實需要全局狀態共享的場景中使用它。
4. 什麼是 jotai 庫,它如何幫助管理應用程序的狀態?#
Jotai 是一個輕量級的狀態管理庫,專為 React 應用程序設計。它基於原子(atoms)和選擇器(selectors)的概念,使狀態管理變得簡單和高效。Jotai 的核心思想是將狀態分解為最小的、可組合的單元(原子),從而使得狀態易於管理和跟蹤。與 Redux 或 MobX 等其他狀態管理庫相比,Jotai 更加輕量級且易於學習。
使用 Jotai 和使用 Context API 相比,其優點在於更加簡單、靈活和易於維護。以下是一些原因:
-
簡單易用。
使用 Jotai 只需要創建原子(atom)並使用 React Hooks 即可進行狀態管理。相對於 Context API,使用 Jotai 的代碼更加簡單易用。 -
高度靈活。
Jotai 允許你隨意組合和複合不同的原子來創建更複雜的狀態,從而使得狀態管理更加靈活和可擴展。相對於 Context API,使用 Jotai 的靈活性更高。 -
更好的性能。
使用 Jotai 可以避免 Context API 中因為使用 Provider 和 Consumer 組件造成的無用渲染,從而提高應用的性能。Jotai 會自動優化組件的重新渲染,並且只在原子狀態發生變化時才會更新相關組件。 -
更易於維護。
使用 Jotai 可以使得狀態管理更加清晰、明確和易於維護。通過將狀態分解為多個原子,每個原子只包含一個狀態值,可以更好地控制狀態的變化和維護應用的狀態。
使用 Jotai 可以使得狀態管理更加簡單、靈活、易於維護,並且具有更好的性能表現。當然,使用 Context API 也可以進行狀態管理,而且更加原生,但是在處理複雜狀態時可能需要編寫更多的代碼,並且容易造成性能問題。因此,在選擇狀態管理庫時,需要根據具體情況進行選擇。
數據傳遞和處理#
1. 如何在 React 組件之間傳遞數據(props)?#
- 父組件向子組件傳遞數據
在父組件中使用子組件時,可以通過在子組件上添加屬性來傳遞數據。例如:
function Parent() {
const data = {name: 'John', age: 30};
return <Child data={data} />;
}
function Child(props) {
return (
<div>
<p>Name: {props.data.name}</p>
<p>Age: {props.data.age}</p>
</div>
);
}
在這個例子中,父組件 Parent 向子組件 Child 傳遞了一個名為 data 的對象,子組件可以通過 props.data 來訪問這個對象。
- 子組件向父組件傳遞數據
在子組件中,可以通過調用父組件傳遞的函數來向父組件傳遞數據。例如:
function Parent() {
function handleChildData(data) {
console.log(data);
}
return <Child onData={handleChildData} />;
}
function Child(props) {
function handleClick() {
props.onData('Hello, parent!');
}
return <button onClick={handleClick}>Click me</button>;
}
在這個例子中,子組件 Child 通過調用 props.onData 函數來向父組件傳遞數據。
- 兄弟組件之間傳遞數據
在兄弟組件之間傳遞數據可以通過在它們的共同父組件中定義狀態,然後將狀態作為 props 屬性傳遞給它們。例如:
function Parent() {
const [data, setData] = useState('Hello, world!');
return (
<>
<Sibling1 data={data} />
<Sibling2 setData={setData} />
</>
);
}
function Sibling1(props) {
return <p>{props.data}</p>;
}
function Sibling2(props) {
function handleClick() {
props.setData('Hello, sibling 1!');
}
return <button onClick={handleClick}>Click me</button>;
}
在這個例子中,Sibling1 和 Sibling2 是兄弟組件,它們之間通過共同的父組件 Parent 中的狀態 data 進行通信,Sibling1 通過 props.data 屬性獲取數據,Sibling2 通過 props.setData 函數來更新數據。
2. 什麼是 React 中的 "lift state up"(狀態提升)模式?為什麼它對數據傳遞和處理很重要?#
"狀態提升"(lift state up)是 React 中一種常見的模式,用於處理組件之間的數據傳遞和狀態管理。這種模式的主要思想是將組件之間共享的狀態提升到它們的共同父組件中進行管理,以便更好地管理和協調組件之間的數據流動。
通過將狀態提升到共同的父組件中,可以將狀態作為 props 傳遞給子組件,從而在組件之間共享數據。這可以使得組件之間的數據傳遞更加清晰和直觀,避免了組件之間互相依賴和相互修改狀態的問題。此外,這種模式也可以減少重複的狀態管理代碼,從而使代碼更加簡潔和易於維護。
"狀態提升" 模式對於處理組件之間的數據傳遞和狀態管理非常重要。在 React 中,組件之間的數據傳遞通常是通過 props 屬性來實現的。當組件需要訪問共享狀態時,可以將這些狀態提升到它們的共同父組件中進行管理,並將狀態作為 props 屬性傳遞給子組件。這種模式可以使得組件之間的數據傳遞更加清晰和直觀,避免了組件之間互相依賴和相互修改狀態的問題。
除此之外,"狀態提升" 模式還可以使得代碼更加可靠和可維護。通過將狀態提升到共同的父組件中進行管理,可以減少重複的狀態管理代碼,並將狀態邏輯封裝在父組件中,從而使得代碼更加簡潔和易於維護。
3. 如何處理異步數據加載和更新(例如從 API 獲取數據)?#
- 使用 useEffect Hook
可以使用 useEffect Hook 來處理異步數據加載和更新。在 useEffect 中,可以使用異步函數來獲取數據,並使用 useState Hook 來保存數據和更新狀態。例如:
import { useEffect, useState } from 'react';
function App() {
const [data, setData] = useState([]);
useEffect(() => {
async function fetchData() {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
setData(data);
}
fetchData();
}, []);
return (
<ul>
{data.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
);
}
在這個例子中,通過 useEffect Hook 來異步獲取數據,並使用 useState Hook 來保存數據和更新狀態。useEffect 的第二個參數為空數組,表示只在組件掛載時執行一次。
- 使用事件回調
可以在組件內部使用事件回調來處理異步數據加載和更新。例如:
import { useState } from 'react';
function App() {
const [data, setData] = useState([]);
async function fetchData() {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
setData(data);
}
return (
<div>
<button onClick={fetchData}>Load data</button>
<ul>
{data.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
);
}
在這個例子中,使用事件回調來處理異步數據加載和更新。點擊按鈕時,調用 fetchData 函數來重新獲取數據,並將數據保存在狀態中。
使用那種方式需要看具體的需求場景,正確區分 Event 與 Effect 是非常重要的,可以參考這篇文檔:separating-events-from-effects
4. 什麼是受控組件和非受控組件?它們在數據處理中的應用場景分別是什麼?#
受控組件和非受控組件,這兩個概念通常是針對表單元素(如輸入框、選擇框和單選按鈕等)而言的。
然而,實際上這兩個概念也可以擴展到非表單元素的組件。關鍵在於如何管理組件內部的狀態,以及如何處理來自外部的數據。以下是一個簡單的例子,說明如何將受控和非受控概念應用於非表單元素的組件:
受控組件(非表單元素):
import React from 'react';
function ControlledDiv({ content, onContentChange }) {
const handleClick = () => {
onContentChange('New Content');
};
return <div onClick={handleClick}>{content}</div>;
}
在這個例子中,ControlledDiv
組件接收一個 content
屬性和一個 onContentChange
回調函數。當用戶點擊這個組件時,它會觸發回調函數來更新外部傳入的 content
。這意味著組件內部的狀態由外部控制,因此可以將其視為受控組件。
非受控組件(非表單元素):
import React, { useState } from 'react';
function UncontrolledDiv() {
const [content, setContent] = useState('Initial Content');
const handleClick = () => {
setContent('New Content');
};
return <div onClick={handleClick}>{content}</div>;
}
在這個例子中,UncontrolledDiv
組件內部維護了一個 content
狀態。當用戶點擊這個組件時,它會直接更新內部的狀態而不需要從外部獲取數據。因此,這個組件可以被視為非受控組件。
總之,儘管受控組件和非受控組件的概念主要用於表單元素,但它們實際上可以擴展到非表單元素的組件,關鍵在於組件狀態的管理和數據處理方式。
5. 如何使用 React 的 useCallback 和 useMemo Hooks 來優化數據處理和函數傳遞?#
useCallback
和 useMemo
是 React 的兩個 Hooks,它們可以幫助優化數據處理和函數傳遞,避免不必要的組件重新渲染。以下是以 useMemo
為例
import { useMemo, memo, useState } from "react";
const ChildComponent = memo(function ChildComponent({ data }) {
console.log("Childcomponent render");
return (
<div>
<p>Name: {data.name}</p>
<p>Age: {data.age}</p>
</div>
);
});
function ParentComponent() {
const [num, setNum] = useState(0);
// 不要學習這個示例,沒有性能問題時不要使用 useMemo useCallback
const data = useMemo(() => {
return { name: "John", age: 30 };
}, []);
return (
<>
<div>
num: {num}{" "}
<button onClick={() => setNum((_num) => _num + 1)}>increase</button>
</div>
<ChildComponent data={data} />
</>
);
}
export default ParentComponent;
在這個例子中,ParentComponent 使用 useMemo 包裹了一個 object,並將其作為 props 傳遞給 memo 包裹的 ChildComponent。由於 ChildComponent 是 memo 包裹的,只有當 data 發生變化時,ChildComponent 才會重新渲染。
當我們點擊 increase 按鈕時,雖然 ParentComponent
發生了 rerender,但是 data 使用 useMemo 包裹,data 的引用未改變,所以 ChildComponent
不會重新渲染。
::: tip
請注意 ChildComponent
必須是 React.memo
包裹的組件上述 useMemo
的優化才會生效。
這是因為當 ParentComponent
rerender 時其子組件就會 rerender,不論其 props 是否發生了改變。只有當其子組件是 React.memo
組件時,React 才會使用 Object.is
比較 props 是否變更來決定是否跳過 rerender。
:::
6. 如何利用 React 的自定義 Hooks 來封裝和復用數據處理邏輯?#
自定義 Hooks 是一種在函數組件中封裝和復用狀態和副作用邏輯的方法。自定義 Hooks 的命名通常以 use 開頭。下面是一個簡單的自定義 Hook 示例:
import React, { useState, useEffect } from 'react';
// 定義一個用於封裝數據處理邏輯的自定義 Hook
function useDataHandling(data) {
const [processedData, setProcessedData] = useState(null);
useEffect(() => {
// 定義數據處理邏輯
function processData(data) {
// ... 數據處理過程 ...
return processedData;
}
// 處理數據並更新狀態
setProcessedData(processData(data));
}, [data]);
// 返回處理後的數據
return processedData;
}
// 在函數組件中使用自定義 Hook
function MyComponent({ data }) {
const processedData = useDataHandling(data);
// ... 使用處理後的數據 ...
}
自定義 Hook 可以幫助讓代碼更加模塊化和清晰。即使不考慮代碼復用,將邏輯拆分到自定義 Hook 中仍然具有一定的優勢:
- 關注點分離:自定義 Hook 可以將組件中的不同關注點(如狀態管理、副作用處理、數據處理等)分離到不同的 Hook 中。這有助於讓組件代碼更加簡潔,易於理解和維護。
- 邏輯解耦:將特定邏輯封裝到一個自定義 Hook 中,可以降低組件之間的耦合程度,使組件更具靈活性。這樣,當需求變化時,修改自定義 Hook 不會影響到其他組件。
- 易於測試:自定義 Hook 可以獨立於組件進行測試。這意味著您可以針對特定的邏輯編寫單元測試,而無需擔心其他組件的影響。
- 更好的可讀性:使用自定義 Hook 可以讓組件代碼更具描述性,因為 Hook 的名稱往往能夠直接反映其功能和作用。這有助於提高代碼的可讀性和可維護性。
因此,在實際開發中,即使某段代碼不會被復用,將其拆分到自定義 Hook 中也是有好處的。在進行代碼重構時,可以考慮將邏輯拆分到合適的自定義 Hook 中,以提高代碼質量。