目錄
- 引言:React Hooks 的革命
- 基礎 Hooks
- useState:狀態管理的新方式
- useEffect:組件生命周期的替代方案
- useContext:簡化 Context API
- 額外的 Hooks
- useReducer:復雜狀態邏輯的管理
- useCallback 與 useMemo:性能優化利器
- useRef:引用 DOM 和保存變量
- 自定義 Hooks:邏輯復用的最佳實踐
- Hooks 使用規則與陷阱
- 實戰案例:使用 Hooks 重構傳統組件
- 總結與展望
引言:React Hooks 的革命
React Hooks 是 React 16.8 版本中引入的特性,它徹底改變了 React 組件的編寫方式。在 Hooks 出現之前,我們需要使用類組件來管理狀態和生命周期,而函數組件則被視為"無狀態組件"。Hooks 的出現使得函數組件也能夠擁有狀態和生命周期功能,從而簡化了組件邏輯,提高了代碼的可讀性和可維護性。
Hooks 解決了 React 中的一些長期存在的問題:
- 組件之間難以復用狀態邏輯:在 Hooks 之前,我們通常使用高階組件(HOC)或 render props 模式來復用組件邏輯,但這些方法往往導致組件嵌套過深,形成"嵌套地獄"。
- 復雜組件變得難以理解:生命周期方法中常常混雜著不相關的邏輯,而相關邏輯卻分散在不同的生命周期方法中。
- 類組件的困惑:類組件需要理解 JavaScript 中
this
的工作方式,這對新手不太友好。
接下來,我們將深入探討 React Hooks 的各個方面,從基礎用法到高級技巧,幫助你全面掌握這一強大特性。
基礎 Hooks
useState:狀態管理的新方式
useState
是最基礎也是最常用的 Hook,它讓函數組件能夠擁有自己的狀態。
import React, { useState } from 'react';function Counter() {// 聲明一個叫 "count" 的 state 變量,初始值為 0const [count, setCount] = useState(0);return (<div><p>你點擊了 {count} 次</p><button onClick={() => setCount(count + 1)}>點擊我</button></div>);
}
useState
返回一個數組,包含兩個元素:當前狀態值和一個更新該狀態的函數。我們使用數組解構來獲取這兩個值。
useState 的高級用法:
- 函數式更新:當新的狀態依賴于之前的狀態時,推薦使用函數式更新。
// 不推薦
setCount(count + 1);// 推薦
setCount(prevCount => prevCount + 1);
- 惰性初始化:如果初始狀態需要通過復雜計算獲得,可以傳遞一個函數給
useState
。
const [state, setState] = useState(() => {const initialState = someExpensiveComputation(props);return initialState;
});
useEffect:組件生命周期的替代方案
useEffect
讓你在函數組件中執行副作用操作,如數據獲取、訂閱或手動更改 DOM 等。它統一了 componentDidMount
、componentDidUpdate
和 componentWillUnmount
這三個生命周期方法。
import React, { useState, useEffect } from 'react';function Example() {const [count, setCount] = useState(0);// 類似于 componentDidMount 和 componentDidUpdateuseEffect(() => {// 更新文檔標題document.title = `你點擊了 ${count} 次`;// 返回一個清理函數,類似于 componentWillUnmountreturn () => {document.title = 'React App';};}, [count]); // 僅在 count 更改時更新return (<div><p>你點擊了 {count} 次</p><button onClick={() => setCount(count + 1)}>點擊我</button></div>);
}
useEffect 的依賴數組:
- 空數組
[]
:效果只在組件掛載和卸載時執行一次,類似于componentDidMount
和componentWillUnmount
。 - 有依賴項
[a, b]
:效果在組件掛載時以及依賴項變化時執行。 - 無依賴數組:效果在每次渲染后執行。
useContext:簡化 Context API
useContext
讓你可以訂閱 React 的 Context,而不必使用 Context.Consumer 組件。
import React, { useContext } from 'react';// 創建一個 Context
const ThemeContext = React.createContext('light');function ThemedButton() {// 使用 useContext 獲取當前主題const theme = useContext(ThemeContext);return (<button style={{ background: theme === 'dark' ? '#333' : '#fff', color: theme === 'dark' ? '#fff' : '#333' }}>我是一個主題按鈕</button>);
}function App() {return (<ThemeContext.Provider value="dark"><ThemedButton /></ThemeContext.Provider>);
}
useContext
接收一個 Context 對象(由 React.createContext
創建)并返回該 Context 的當前值。當 Provider 更新時,使用該 Context 的組件會重新渲染。
額外的 Hooks
useReducer:復雜狀態邏輯的管理
useReducer
是 useState
的替代方案,適用于有復雜狀態邏輯的場景,特別是當下一個狀態依賴于之前的狀態時。
import React, { useReducer } from 'react';// 定義 reducer 函數
function reducer(state, action) {switch (action.type) {case 'increment':return { count: state.count + 1 };case 'decrement':return { count: state.count - 1 };default:throw new Error();}
}function Counter() {// 使用 useReducerconst [state, dispatch] = useReducer(reducer, { count: 0 });return (<div>Count: {state.count}<button onClick={() => dispatch({ type: 'increment' })}>+</button><button onClick={() => dispatch({ type: 'decrement' })}>-</button></div>);
}
useReducer
返回當前狀態和 dispatch
函數。dispatch
函數用于觸發狀態更新,它接收一個 action 對象,通常包含 type
屬性和可選的 payload。
useCallback 與 useMemo:性能優化利器
這兩個 Hooks 主要用于性能優化,避免不必要的計算和渲染。
useCallback:返回一個記憶化的回調函數,只有當依賴項變化時才會更新。
import React, { useState, useCallback } from 'react';function ParentComponent() {const [count, setCount] = useState(0);// 使用 useCallback 記憶化回調函數const handleClick = useCallback(() => {console.log(`Button clicked, count: ${count}`);}, [count]); // 只有當 count 變化時,handleClick 才會更新return (<div><ChildComponent onClick={handleClick} /><button onClick={() => setCount(count + 1)}>Increment</button></div>);
}// 使用 React.memo 優化子組件
const ChildComponent = React.memo(({ onClick }) => {console.log('ChildComponent rendered');return <button onClick={onClick}>Click me</button>;
});
useMemo:返回一個記憶化的值,只有當依賴項變化時才重新計算。
import React, { useState, useMemo } from 'react';function ExpensiveCalculation({ a, b }) {// 使用 useMemo 記憶化計算結果const result = useMemo(() => {console.log('Computing result...');// 假設這是一個耗時的計算return a * b;}, [a, b]); // 只有當 a 或 b 變化時,才重新計算return <div>Result: {result}</div>;
}
useRef:引用 DOM 和保存變量
useRef
返回一個可變的 ref 對象,其 .current
屬性被初始化為傳入的參數。返回的 ref 對象在組件的整個生命周期內保持不變。
引用 DOM 元素:
import React, { useRef, useEffect } from 'react';function TextInputWithFocusButton() {// 創建一個 refconst inputRef = useRef(null);// 點擊按鈕時聚焦輸入框const focusInput = () => {inputRef.current.focus();};return (<div><input ref={inputRef} type="text" /><button onClick={focusInput}>聚焦輸入框</button></div>);
}
保存變量:
import React, { useState, useRef, useEffect } from 'react';function Timer() {const [count, setCount] = useState(0);// 使用 useRef 保存 interval IDconst intervalRef = useRef(null);useEffect(() => {// 設置定時器intervalRef.current = setInterval(() => {setCount(c => c + 1);}, 1000);// 清理定時器return () => {clearInterval(intervalRef.current);};}, []); // 空依賴數組,只在掛載和卸載時執行// 停止計時器const stopTimer = () => {clearInterval(intervalRef.current);};return (<div><p>計數: {count}</p><button onClick={stopTimer}>停止</button></div>);
}
與 useState
不同,useRef
的 .current
屬性變化不會觸發組件重新渲染。
自定義 Hooks:邏輯復用的最佳實踐
自定義 Hooks 是 React Hooks 最強大的特性之一,它允許你將組件邏輯提取到可重用的函數中。自定義 Hook 是一個以 “use” 開頭的 JavaScript 函數,可以調用其他 Hooks。
示例:創建一個 useLocalStorage Hook
import { useState, useEffect } from 'react';// 自定義 Hook:使用 localStorage 持久化狀態
function useLocalStorage(key, initialValue) {// 初始化狀態const [storedValue, setStoredValue] = useState(() => {try {// 嘗試從 localStorage 獲取值const item = window.localStorage.getItem(key);// 如果存在則解析并返回,否則返回初始值return item ? JSON.parse(item) : initialValue;} catch (error) {console.log(error);return initialValue;}});// 更新 localStorage 的函數const setValue = value => {try {// 允許值是一個函數,類似于 useStateconst valueToStore = value instanceof Function ? value(storedValue) : value;// 保存到 statesetStoredValue(valueToStore);// 保存到 localStoragewindow.localStorage.setItem(key, JSON.stringify(valueToStore));} catch (error) {console.log(error);}};return [storedValue, setValue];
}// 使用自定義 Hook
function App() {const [name, setName] = useLocalStorage('name', 'Bob');return (<div><inputtype="text"value={name}onChange={e => setName(e.target.value)}/></div>);
}
示例:創建一個 useWindowSize Hook
import { useState, useEffect } from 'react';// 自定義 Hook:獲取窗口尺寸
function useWindowSize() {// 初始化狀態const [windowSize, setWindowSize] = useState({width: undefined,height: undefined,});useEffect(() => {// 處理窗口大小變化的函數function handleResize() {setWindowSize({width: window.innerWidth,height: window.innerHeight,});}// 添加事件監聽器window.addEventListener('resize', handleResize);// 初始調用一次以設置初始值handleResize();// 清理函數return () => window.removeEventListener('resize', handleResize);}, []); // 空依賴數組,只在掛載和卸載時執行return windowSize;
}// 使用自定義 Hook
function ResponsiveComponent() {const size = useWindowSize();return (<div>{size.width < 768 ? (<p>在小屏幕上顯示</p>) : (<p>在大屏幕上顯示</p>)}</div>);
}
Hooks 使用規則與陷阱
使用 Hooks 時,必須遵循兩條重要規則:
- 只在最頂層使用 Hooks:不要在循環、條件或嵌套函數中調用 Hooks,確保 Hooks 在每次組件渲染時都以相同的順序被調用。
// ? 錯誤:在條件語句中使用 Hook
function Form() {const [name, setName] = useState('Mary');if (name !== '') {useEffect(() => {localStorage.setItem('name', name);});}// ...
}// ? 正確:將條件放在 Hook 內部
function Form() {const [name, setName] = useState('Mary');useEffect(() => {if (name !== '') {localStorage.setItem('name', name);}});// ...
}
- 只在 React 函數組件或自定義 Hooks 中調用 Hooks:不要在普通的 JavaScript 函數中調用 Hooks。
常見陷阱與解決方案:
- 依賴數組遺漏:
// ? 錯誤:依賴數組遺漏了 count
function Counter({ count }) {useEffect(() => {document.title = `Count: ${count}`;}, []); // 依賴數組為空,但使用了 count// ...
}// ? 正確:添加所有依賴項
function Counter({ count }) {useEffect(() => {document.title = `Count: ${count}`;}, [count]); // 正確添加了 count 作為依賴項// ...
}
- 閉包陷阱:
// ? 問題:使用過時的狀態值
function Counter() {const [count, setCount] = useState(0);useEffect(() => {const timer = setInterval(() => {console.log(`Current count: ${count}`);setCount(count + 1); // 這里的 count 是閉包捕獲的初始值}, 1000);return () => clearInterval(timer);}, []); // 依賴數組為空,導致 count 始終為初始值 0// ...
}// ? 解決方案:使用函數式更新
function Counter() {const [count, setCount] = useState(0);useEffect(() => {const timer = setInterval(() => {setCount(prevCount => prevCount + 1); // 使用函數式更新獲取最新狀態}, 1000);return () => clearInterval(timer);}, []); // 現在可以安全地使用空依賴數組// ...
}
- 過度依賴 useEffect:
// ? 不必要的 useEffect
function Form() {const [firstName, setFirstName] = useState('');const [lastName, setLastName] = useState('');// 不必要的 useEffect,可以直接計算const [fullName, setFullName] = useState('');useEffect(() => {setFullName(`${firstName} ${lastName}`);}, [firstName, lastName]);// ...
}// ? 更好的方式:直接計算派生狀態
function Form() {const [firstName, setFirstName] = useState('');const [lastName, setLastName] = useState('');// 直接計算派生值,無需額外的狀態和 useEffectconst fullName = `${firstName} ${lastName}`;// ...
}
實戰案例:使用 Hooks 重構傳統組件
讓我們通過一個實際例子,展示如何將類組件重構為使用 Hooks 的函數組件。
原始類組件:
import React, { Component } from 'react';class UserProfile extends Component {constructor(props) {super(props);this.state = {user: null,loading: true,error: null};}componentDidMount() {this.fetchUserData();}componentDidUpdate(prevProps) {if (prevProps.userId !== this.props.userId) {this.fetchUserData();}}fetchUserData = async () => {this.setState({ loading: true });try {const response = await fetch(`https://api.example.com/users/${this.props.userId}`);const data = await response.json();this.setState({ user: data, loading: false });} catch (error) {this.setState({ error, loading: false });}};render() {const { user, loading, error } = this.state;if (loading) return <div>Loading...</div>;if (error) return <div>Error: {error.message}</div>;if (!user) return <div>No user data</div>;return (<div><h1>{user.name}</h1><p>Email: {user.email}</p><p>Phone: {user.phone}</p></div>);}
}
使用 Hooks 重構后的函數組件:
import React, { useState, useEffect } from 'react';function UserProfile({ userId }) {const [user, setUser] = useState(null);const [loading, setLoading] = useState(true);const [error, setError] = useState(null);useEffect(() => {async function fetchUserData() {setLoading(true);try {const response = await fetch(`https://api.example.com/users/${userId}`);const data = await response.json();setUser(data);setLoading(false);} catch (error) {setError(error);setLoading(false);}}fetchUserData();}, [userId]); // 依賴項數組,只有當 userId 變化時才重新獲取數據if (loading) return <div>Loading...</div>;if (error) return <div>Error: {error.message}</div>;if (!user) return <div>No user data</div>;return (<div><h1>{user.name}</h1><p>Email: {user.email}</p><p>Phone: {user.phone}</p></div>);
}
進一步優化:提取自定義 Hook
import React, { useState, useEffect } from 'react';// 自定義 Hook:獲取用戶數據
function useUserData(userId) {const [user, setUser] = useState(null);const [loading, setLoading] = useState(true);const [error, setError] = useState(null);useEffect(() => {async function fetchUserData() {setLoading(true);try {const response = await fetch(`https://api.example.com/users/${userId}`);const data = await response.json();setUser(data);setLoading(false);} catch (error) {setError(error);setLoading(false);}}fetchUserData();}, [userId]);return { user, loading, error };
}// 使用自定義 Hook 的組件
function UserProfile({ userId }) {const { user, loading, error } = useUserData(userId);if (loading) return <div>Loading...</div>;if (error) return <div>Error: {error.message}</div>;if (!user) return <div>No user data</div>;return (<div><h1>{user.name}</h1><p>Email: {user.email}</p><p>Phone: {user.phone}</p></div>);
}
通過這個例子,我們可以看到 Hooks 帶來的幾個明顯優勢:
- 代碼更簡潔:函數組件比類組件代碼量少,更易于閱讀和理解。
- 關注點分離:通過自定義 Hook,我們可以將數據獲取邏輯與 UI 渲染邏輯分離。
- 邏輯復用:自定義 Hook 可以在多個組件之間復用,而不需要使用 HOC 或 render props。
總結與展望
React Hooks 徹底改變了 React 組件的編寫方式,使函數組件成為了 React 開發的主流。通過本文,我們深入探討了 React Hooks 的基礎知識、高級用法、常見陷阱以及最佳實踐。
Hooks 的優勢總結:
- 簡化組件邏輯:使用 Hooks,我們可以將相關的邏輯放在一起,而不是分散在不同的生命周期方法中。
- 促進邏輯復用:自定義 Hooks 提供了一種比 HOC 和 render props 更簡潔的邏輯復用方式。
- 更好的類型推斷:在 TypeScript 中,Hooks 比類組件有更好的類型推斷。
- 減少代碼量:函數組件通常比等效的類組件代碼量少。
- 更容易測試:純函數更容易測試,Hooks 使得編寫純函數組件變得更加容易。
隨著 React 的發展,Hooks 生態系統也在不斷壯大。React 團隊還在探索更多的 Hooks,如 useTransition
和 useDeferredValue
,以解決并發模式下的性能問題。同時,社區也開發了大量的第三方 Hooks 庫,如 react-use
、use-http
等,進一步擴展了 Hooks 的能力。
參考資料:
- React 官方文檔 - Hooks 介紹
- React Hooks 完全指南
- 使用 React Hooks 的常見錯誤
- 深入理解 React useEffect