在React 16.8版本之前,組件主要分為兩種:類組件(Class Components) 和 函數組件(Function Components)。類組件可以使用 state
來管理內部狀態,也能使用生命周期方法(如 componentDidMount
)來處理副作用。而函數組件是“無狀態的”,它們只能接收 props
并返回JSX,無法擁有自己的狀態和生命周期。
React Hooks 是一系列特殊的函數,它們允許你在函數組件中“鉤入”React 的 state 及生命周期等特性。 簡單來說,Hooks 讓函數組件也能擁有和類組件幾乎同等的能力,從此你可以在不編寫 class 的情況下使用 state 和其他 React 功能。這使得函數組件成為現代React開發的首選。
使用hooks的原則:
- 只在頂層調用 Hooks:不要在循環、條件判斷或嵌套函數中調用 Hooks。必須保證 Hooks 在每次組件渲染時的調用順序都是完全一致的。這是因為 React 依賴于 Hooks 的調用順序來正確地將 state 與對應的
useState
或useEffect
關聯起來。 - 只在 React 函數中調用 Hooks:你只能在 React 函數組件 或 自定義 Hooks 中調用 Hooks。不能在普通的 JavaScript 函數中調用。
數據驅動更新型:
數據更新useState:
適用于管理組件的局部狀態,如開關的開/關狀態、表單輸入的值、一個計數器的值等。當狀態邏輯簡單,且不依賴于其他復雜狀態時,useState
是最佳選擇。
表單
:
import React, { useState } from 'react';function NameInput() {// 聲明一個名為 name 的狀態,初始值為空字符串const [name, setName] = useState('');const handleChange = (event) => {// 調用 setName 更新狀態,觸發重新渲染setName(event.target.value);};return (<div><input type="text" value={name} onChange={handleChange} /><p>Hello, {name}!</p></div>);
}
訂閱更新useReducer:
當狀態邏輯變得復雜,或者下一個狀態依賴于前一個狀態時,useReducer
是 useState
的一個更強大的替代方案。它借鑒了 Redux 的思想,通過 dispatch
一個 action
來集中管理狀態的更新邏輯。
在事件處理中,調用 dispatch({ type: 'ACTION_TYPE', payload: ... })
來觸發狀態更新。
import React, { useReducer } from 'react';// 1. Reducer 函數:定義所有可能的狀態轉換
const counterReducer = (state, action) => {switch (action.type) {case 'increment':return { count: state.count + 1 };case 'decrement':return { count: state.count - 1 };case 'reset':return { count: 0 };default:throw new Error();}
};function Counter() {// 2. 使用 useReducer 初始化狀態const [state, dispatch] = useReducer(counterReducer, { count: 0 });return (<div><p>Count: {state.count}</p>{/* 3. Dispatch actions 來觸發更新 */}<button onClick={() => dispatch({ type: 'increment' })}>+</button><button onClick={() => dispatch({ type: 'decrement' })}>-</button><button onClick={() => dispatch({ type: 'reset' })}>Reset</button></div>);
}
狀態的獲取和傳遞值:
訂閱獲取上下文useContext:
在 React 應用中,數據通常是通過 props
從父組件單向地流向子組件。但如果一個狀態需要被深層次的子組件使用,或者被多個不同層級的組件共享,通過 props 層層傳遞(這個過程被稱為 “prop drilling” 或“屬性鉆探”)會變得非常繁瑣和難以維護。
- 創建 Context: 使用
React.createContext()
創建一個 Context 對象。這個對象就像一個信息頻道。
const MyContext = React.createContext(defaultValue);
- 提供 Context: 在組件樹的上層,使用
<MyContext.Provider>
組件,通過value
屬性來“廣播”你想要共享的數據。所有被這個 Provider 包裹的子組件(無論層級多深)都能訪問到這個value
。
<MyContext.Provider value={/* 你想共享的任何值 */}><App />
</MyContext.Provider>
- 消費 Context: 在任何一個子組件中,調用
useContext(MyContext)
Hook 來“訂閱”并讀取這個value
const value = useContext(MyContext);
全局狀態管理:如應用的主題(白天/黑夜模式)、當前的登錄用戶信息、語言偏好設置等。
import React, { useState, useContext, createContext } from 'react';// 1. 創建一個主題 Context,可以給一個默認值
const ThemeContext = createContext('light');// App 組件作為頂層組件
function App() {const [theme, setTheme] = useState('light');const toggleTheme = () => {setTheme(current => (current === 'light' ? 'dark' : 'light'));};// 2. 使用 Provider 將 theme 和 toggleTheme 函數提供給所有子組件return (<ThemeContext.Provider value={{ theme, toggleTheme }}><Toolbar /></ThemeContext.Provider>);
}// Toolbar 組件,它本身不需要 theme,只是一個中間組件
function Toolbar() {return (<div><ThemedButton /></div>);
}// ThemedButton 是真正需要使用 theme 的深層子組件
function ThemedButton() {// 3. 使用 useContext 直接獲取共享的主題和方法const { theme, toggleTheme } = useContext(ThemeContext);const style = {background: theme === 'dark' ? '#333' : '#eee',color: theme === 'dark' ? '#fff' : '#333',padding: '10px',border: 'none',borderRadius: '5px'};return (<button style={style} onClick={toggleTheme}>當前是 {theme} 主題,點我切換</button>);
}export default App;
元素組件獲取useRef:
useRef
是一個非常獨特的 Hook。雖然它也用于在組件中存儲數據,但它與 useState
有一個本質區別:更新 useRef
的值不會觸發組件的重新渲染。
用途:
1.訪問 DOM 元素
這是 useRef
的首要用途。你可以創建一個 ref,并將它附加到 JSX 元素的 ref
屬性上,之后就可以通過這個 ref 直接訪問該 DOM 節點。
import React, { useRef } from 'react';function FocusInput() {// 1. 創建一個 ref 對象const inputRef = useRef(null);const handleFocus = () => {// 3. 通過 .current 屬性訪問 DOM 節點并調用其方法if (inputRef.current) {inputRef.current.focus();}};return (<div>{/* 2. 將 ref 附加到 input 元素上 */}<input ref={inputRef} type="text" /><button onClick={handleFocus}>Focus the input</button></div>);
}
2.存儲一個可變的引用值:
因為 useRef
的值在每次渲染時都保持不變,且更新它不會觸發重渲染,所以它也可以作為一個“實例變量”,用來存儲那些你需要在多次渲染之間共享、但又不想觸發視圖更新的數據。更新.current
屬性不會觸發任何重渲染
import react, { useState, useEffect, useRef } from 'react';function RenderCounter() {const [count, setCount] = useState(0);// 使用 ref 來存儲渲染次數const renderCount = useRef(0);useEffect(() => {// 每次渲染后,renderCount 的值加一// 注意:更新 ref 不會觸發另一次渲染,避免了無限循環renderCount.current = renderCount.current + 1;});return (<div><p>State Count: {count}</p><p>This component has rendered {renderCount.current} times.</p> <button onClick={() => setCount(c => c + 1)}>Trigger Re-render</button></div>);
}
狀態派生和保存型:
- 當你需要緩存一個計算結果(如一個經過過濾的數組,一個復雜的計算值)時,用
useMemo
。 - 當你需要緩存一個函數本身(通常是為了作為 prop 傳遞)時,用
useCallback
。
派生新狀態useMemo:
useMemo
的核心作用是 “記憶”一個計算結果。在 React 中,當一個組件的 state
或 props
改變時,整個組件函數會重新執行,這意味著函數內部的所有代碼(包括一些復雜的計算)都會被重新運行。如果某個計算非常耗時,這就會導致界面卡頓,影響用戶體驗。只有當其依賴項發生變化時,它才會重新執行計算,否則它會直接返回上一次緩存的結果。
適用場景:
- 對一個巨大的列表進行排序或過濾。
- 在組件中進行復雜的數學運算或數據處理。
- 當一個子組件的 props 需要通過復雜計算得出時,用
useMemo
來穩定這個 prop,防止子組件不必要的重新渲染。
import React, { useState, useMemo } from 'react';// 假設 allUsers 是一個包含 1000 個對象的巨大數組
const allUsers = [...]; function UserList() {const [searchTerm, setSearchTerm] = useState('');const [anotherState, setAnotherState] = useState(false);// 沒有優化的寫法:// 無論 searchTerm 變不變,只要組件重渲染(比如點擊 Toggle 按鈕),// filter 這個昂貴操作就會被重新執行一次。// const filteredUsers = allUsers.filter(user => user.name.includes(searchTerm));// 使用 useMemo 優化:// 這個 filter 操作現在被“記憶”了。const filteredUsers = useMemo(() => {console.log('Filtering logic is running...'); // 你會發現只有在 searchTerm 改變時才會打印return allUsers.filter(user => user.name.includes(searchTerm));}, [searchTerm]); // 依賴項是 searchTermreturn (<div><input type="text" placeholder="Search users..." onChange={e => setSearchTerm(e.target.value)} />{/* 這個按鈕的點擊只會更新 anotherState,不會觸發上面的 filter 計算 */}<button onClick={() => setAnotherState(!anotherState)}>Toggle</button><ul>{filteredUsers.map(user => <li key={user.id}>{user.name}</li>)}</ul></div>);
}
保存狀態useCallback:
useCallback是什么?
useCallback
的核心作用是 “記憶”一個函數。在 JavaScript 中,函數是對象。在 React 組件每次重新渲染時,在函數組件內部定義的所有函數都會被重新創建。這意味著,即使函數體內的代碼完全一樣,前后兩次渲染生成的函數在內存中也是兩個不同的引用。- 當一個函數作為
prop
傳遞給一個被React.memo
優化的子組件時,這個問題就變得很關鍵。因為父組件的每次重渲染都會創建一個新的函數實例,導致子組件接收到的prop
(那個函數) 每次都“不相等”,從而使得React.memo
的優化失效,子組件依然會不必要地重新渲染。 useCallback
就是用來解決這個問題的。它會緩存你提供的函數實例,只有當其依賴項改變時,才會重新創建一個新的函數實例
import React, { useState, useCallback } from 'react';// 使用 React.memo 優化子組件,只有 props 變化時才重渲染
const MemoizedButton = React.memo(({ onClick, children }) => {console.log(`Button "${children}" is rendering...`);return <button onClick={onClick}>{children}</button>;
});function ParentComponent() {const [count, setCount] = useState(0);const [anotherState, setAnotherState] = useState(0);// 沒有優化的寫法:// 每次 ParentComponent 重渲染,都會創建一個新的 handleIncrement 函數。// const handleIncrement = () => setCount(count + 1);// 使用 useCallback 優化:// handleIncrement 函數被緩存了,它的引用只在依賴項變化時才更新。// 因為依賴項是空數組 [],所以它在組件的整個生命周期內都保持不變。const handleIncrement = useCallback(() => {setCount(prevCount => prevCount + 1); // 使用函數式更新,避免依賴 count}, []); // 這個函數依賴 anotherState,所以只有 anotherState 變化時才會重新創建const handleAnotherAction = useCallback(() => {// ... do something with anotherState}, [anotherState]);return (<div><p>Count: {count}</p><button onClick={() => setAnotherState(anotherState + 1)}>Update Another State (will not re-render Increment button)</button><MemoizedButton onClick={handleIncrement}>Increment Count</MemoizedButton><MemoizedButton onClick={handleAnotherAction}>Another Action</MemoizedButton></div>);
}
工具類:
服務端渲染: useId:
useId
是 React 18 中引入的一個新 Hook,它的主要目的是在客戶端和服務端生成穩定且唯一的 ID,以解決服務端渲染(Server-Side Rendering, SSR)和客戶端激活(Hydration)過程中的 ID 不匹配問題。
在 useId
出現之前,如果我們需要為組件生成一個唯一的 ID,生成一個在服務端和客戶端之間穩定、唯一且無沖突的 ID 字符串。
import React, { useId } from 'react';function FormField() {// 調用 useId 生成一個在 SSR 和 CSR 中都穩定的唯一 IDconst id = useId();console.log('Generated ID:', id); // 在服務端和客戶端會打印出相同的 ID,例如 ":r1:"return (<div>{/* 使用生成的 id 來關聯 label 和 input */}<label htmlFor={id}>Your Name:</label><input id={id} type="text" name="name" /></div>);
}// 如果一個組件需要多個 ID
function ComplexFormField() {const id = useId();return (<div><label htmlFor={`${id}-firstName`}>First Name</label><input id={`${id}-firstName`} type="text" /><label htmlFor={`${id}-lastName`}>Last Name</label><input id={`${id}-lastName`} type="text" /></div>);
}
執行副作用型:
異步執行副作用useEffect:
這是處理副作用最常用、也是你最應該首先考慮的 Hook。useEffect
允許你在組件渲染到屏幕之后,執行一些與渲染本身無關的操作,比如數據獲取、設置訂閱、手動操作 DOM 等。“異步執行” 這個描述非常關鍵。useEffect
的執行時機是在 React 完成 DOM 更新并將其繪制到屏幕上之后。這意味著 useEffect
內部的代碼不會阻塞瀏覽器的繪制過程,從而保證了用戶界面的流暢和響應性。
執行流程:
- React 渲染組件。
- 瀏覽器更新 DOM 并且**繪制(Paint)**界面。
- 然后,
useEffect
內部的函數被執行。
import React, { useState, useEffect } from 'react';function UserProfile({ userId }) {const [user, setUser] = useState(null);useEffect(() => {// 這個函數會在組件渲染到屏幕上之后被調用console.log('Component has been painted to the screen.');async function fetchUserData() {console.log('Starting data fetch...');const response = await fetch(`https://api.example.com/users/${userId}`);const userData = await response.json();setUser(userData);console.log('Data fetch complete.');}fetchUserData();// 清理函數:在組件卸載或下一次 effect 執行前運行return () => {console.log('Cleaning up previous effect.');};}, [userId]); // 依賴數組,僅在 userId 變化時重新執行if (!user) {return <div>Loading...</div>;}return <h1>{user.name}</h1>;
}
同步執行副作用: useLayoutEffect:
useLayoutEffect
在 API 上與 useEffect
完全相同,但它們的執行時機截然不同。這微小的差異導致了其用途的巨大區別。 “同步執行” 是 useLayoutEffect
的關鍵。它會在 React 計算完所有 DOM 變更之后,但在瀏覽器將這些變更繪制到屏幕上之前同步執行。
執行流程:
- React 渲染組件,并計算出 DOM 的變更。
- 在瀏覽器繪制前,
useLayoutEffect
內部的函數被同步執行。 useLayoutEffect
內部的代碼可能會再次觸發狀態更新,導致組件同步地重新渲染。- 然后,瀏覽器才將最終的 DOM 變更繪制到屏幕上。
因為它是同步執行的,所以如果內部邏輯非常耗時,它會阻塞瀏覽器的繪制,導致頁面卡頓。因此,應該謹慎使用。
只有當你需要在瀏覽器繪制前,讀取 DOM 布局信息并同步地使用這些信息來改變 DOM 時,才應該使用 useLayoutEffect
。這樣做是為了防止用戶看到“閃爍”(Flicker)現象——即組件先以一種狀態渲染,然后又立即變為另一種狀態。
import React, { useState, useLayoutEffect, useRef } from 'react';function AutoWidthInput() {const [width, setWidth] = useState(0);const divRef = useRef(null);// 使用 useLayoutEffect 來防止閃爍useLayoutEffect(() => {// 這個 effect 會在 DOM 更新后、瀏覽器繪制前執行if (divRef.current) {console.log('Reading layout before paint.');// 讀取 div 的寬度const measuredWidth = divRef.current.offsetWidth;// 同步更新 statesetWidth(measuredWidth);}}, []); // 空數組表示只在掛載后執行一次// 如果這里用的是 useEffect,你可能會短暫地看到 input 寬度為 0,然后才跳到正確寬度,造成閃爍。return (<div><div ref={divRef} style={{ width: '200px', marginBottom: '10px', background: 'lightblue' }}>Measure my width!</div><input type="text" style={{ width: `${width}px` }} placeholder="My width matches the div" /></div>);
}