2025 React 狀態(tài)管理終極指南!
React 作為當下最受歡迎的前端框架,在構建復雜且交互豐富的應用時,狀態(tài)管理無疑是至關重要的一環(huán)。從簡單的本地狀態(tài),到能讓多個組件協(xié)同工作的全局狀態(tài),再到涉及服務器通信、導航切換、表單操作以及持久化存儲等不同場景下的狀態(tài)管理,每一個方面都影響著應用的性能、用戶體驗以及可維護性。本文將作為 React 狀態(tài)管理的全面指南,帶你深入了解這些不同類型狀態(tài)的管理方式與要點。
圖片
本地狀態(tài)
- 定義: 在 React 中,本地狀態(tài)是指組件內(nèi)部管理的數(shù)據(jù),這些數(shù)據(jù)可以影響組件的渲染輸出和行為。本地狀態(tài)是相對于全局狀態(tài)而言的,它只存在于單個組件中,用于存儲那些不需要在整個應用范圍內(nèi)共享的信息。
- 特點:
- 私有性:本地狀態(tài)是特定于某個組件的,其他組件無法直接訪問或修改它。
- 局部性:狀態(tài)的變化只會影響該組件及其子組件,而不會影響到父組件或其他兄弟組件。
生命周期性:隨著組件的掛載、更新和卸載,本地狀態(tài)也會經(jīng)歷相應的生命周期階段。
函數(shù)組件
useState
從 React 16.8 開始引入了 Hooks API,使得函數(shù)組件也可以擁有狀態(tài)。useState 是最常用的 Hook 之一,用來聲明和管理組件的狀態(tài)。
- 返回值:調用 useState 時,返回一個包含兩個元素的數(shù)組。第一個元素是當前的狀態(tài)值,可以在組件中直接使用來渲染 UI 等;第二個元素是一個函數(shù),用于更新該狀態(tài)值,調用這個函數(shù)并傳入新的值后,React 會重新渲染組件,使 UI 根據(jù)新的狀態(tài)進行更新。
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>當前計數(shù): {count}</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useReducer
對于那些涉及復雜狀態(tài)邏輯或狀態(tài)更新依賴于前一狀態(tài)的情況,useReducer 可能是一個更好的選擇。它類似于 Redux 的 reducer 函數(shù),但只作用于單個組件內(nèi)部。
- reducer 函數(shù):它是整個狀態(tài)管理的核心邏輯部分,根據(jù)傳入的不同 action 類型,按照預先定義好的規(guī)則來計算并返回新的狀態(tài),就像一個狀態(tài)處理的 “加工廠”,規(guī)范了狀態(tài)更新的流程。
- dispatch 函數(shù):通過調用 dispatch 并傳入相應的 action 對象,可以觸發(fā) reducer 函數(shù)執(zhí)行,從而實現(xiàn)狀態(tài)的更新。這使得狀態(tài)更新的操作更加可預測、可維護,尤其是在復雜的狀態(tài)變化場景中優(yōu)勢明顯。
import React, { useReducer } from 'react';
const initialState = { count: 0 };
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() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
}
自定義Hooks
可以創(chuàng)建自定義 Hook 來封裝特定的狀態(tài)邏輯,并且可以在多個組件之間共享這些邏輯。這不僅提高了代碼的復用性,還使得狀態(tài)管理更加模塊化。
import { useState, useEffect } from 'react';
import axios from 'axios';
const useFetchData = (url, params) => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get(url, { params });
setData(response.data);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
fetchData();
}, [url, params]); // 依賴項包括 URL 和參數(shù),以便在它們變化時重新獲取數(shù)據(jù)
return { data, isLoading, error };
};
export default useFetchData;
類組件
在 Hooks 出現(xiàn)之前,React 類組件通過定義 state 對象并在構造函數(shù)中初始化它來管理狀態(tài)。
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
increment = () => {
this.setState((prevState) => ({ count: prevState.count + 1 }));
};
render() {
return (
<div>
<p>當前計數(shù): {this.state.count}</p>
<button onClick={this.increment}>
Click me
</button>
</div>
);
}
}
注意事項:
- 初始化:通常在類組件的構造函數(shù) constructor 中通過給 this.state 賦值來初始化本地狀態(tài),設定初始的狀態(tài)值。
- 更新狀態(tài):使用 this.setState 方法來更新狀態(tài),它有兩種常見的使用形式。一種是直接傳入一個包含新狀態(tài)值的對象,如 this.setState({ count: 10 });另一種是傳入一個函數(shù),函數(shù)接收前一個狀態(tài) prevState 作為參數(shù),返回新的狀態(tài)對象,這種方式在基于前一狀態(tài)進行計算來更新狀態(tài)時非常有用,能避免一些由于異步操作等帶來的狀態(tài)更新問題。
全局狀態(tài)
- 定義: 在 React 中,全局狀態(tài)是指那些在整個應用中多個組件之間需要共享和訪問的狀態(tài)。與本地狀態(tài)不同,全局狀態(tài)不局限于單個組件,而是可以在應用的不同部分之間傳遞、更新和同步。
- 重要性: 當應用變得越來越大,組件之間的嵌套層次越來越深時,使用 props 逐層傳遞狀態(tài)(即“props drilling”)會變得非常麻煩且難以維護。全局狀態(tài)管理工具可以幫助解決這個問題,它們提供了一種集中管理和共享狀態(tài)的方式,減少了冗余代碼,并提高了開發(fā)效率。
- 使用選擇:
- 狀態(tài)提升: 當需要在兄弟組件中共享狀態(tài)時,可以將狀態(tài)提升到父組件。
- Context API:適合簡單的全局狀態(tài)管理,尤其是當需要避免 props drilling 時。
- Zustand:一個輕量級的選擇,適合小型到中型應用,特別是那些注重性能和易用性的項目。
- Jotai:為更細粒度的狀態(tài)管理提供了可能性,非常適合那些尋求模塊化和高效狀態(tài)管理的開發(fā)者。
狀態(tài)提升
- 定義: 狀態(tài)提升是 React 中一種常見的模式,用于將需要在多個組件之間共享的狀態(tài)移動到它們的最近公共父組件中進行管理。這種方法雖不是全局狀態(tài)管理的一部分,但它提供了一種方式來集中管理跨多個兄弟組件的狀態(tài)。
- 原理: 當兩個或更多的子組件需要訪問相同的數(shù)據(jù)時,可以將該數(shù)據(jù)及其相關的更新邏輯“提升”到這些子組件的共同祖先組件中。然后,通過 props 將數(shù)據(jù)和處理函數(shù)傳遞給需要它的子組件。
- 注意事項:
狀態(tài)更新的流向:狀態(tài)更新總是從父組件開始,父組件更新狀態(tài)后,將新的狀態(tài)作為 props 傳遞給子組件,觸發(fā)子組件的重新渲染。
避免過度提升:不要將所有狀態(tài)都盲目提升,只提升多個組件需要共享的狀態(tài),以保持組件的簡潔和清晰。
Context API
- 定義: React 的 Context API 允許創(chuàng)建一個可以在組件樹中傳遞數(shù)據(jù)的上下文,使得不同層次的組件可以訪問這些數(shù)據(jù),而無需通過 props 一層層傳遞。雖然 Context API 本身并不直接提供狀態(tài)管理功能,但它可以與 Hooks(如 useState)結合使用來實現(xiàn)簡單的全局狀態(tài)管理。
- 基本使用:
提供者: 在 React 19 之前,可以使用 MyContext.Provider的形式來提供共享狀態(tài)。React 19 中,可以直接使用 MyContext的形式,省略掉了.Provider。
import React, { useState, createContext } from 'react';
const MyContext = createContext();
function App() {
const [globalState, setGlobalState] = useState({ count: 0 });
return (
<MyContext.Provider value={{ globalState, setGlobalState }}>
<ComponentA />
<ComponentB />
</MyContext.Provider>
);
}
- 消費者:在子組件中可以使用 useContext 來獲取 MyContext 提供的數(shù)據(jù),實現(xiàn)對全局狀態(tài)的訪問和修改。
import React, { useContext } from 'react';
import MyContext from './MyContext';
function ComponentA() {
const { globalState, setGlobalState } = useContext(MyContext);
const increment = () => {
setGlobalState(prevState => ({...prevState, count: prevState.count + 1 }));
};
return (
<div>
<p>Count: {globalState.count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
Zustand
- 定義: Zustand 是一個輕量級且易于使用的狀態(tài)管理庫,專門為 React 應用設計。它的名字來源于德語單詞 "zustand",意為“狀態(tài)”。Zustand 的設計理念是提供一種簡單、直觀的方式來管理全局狀態(tài),同時保持高性能和小體積。
- 特點:
簡潔性:Zustand 的設計理念是保持極簡主義,通過簡單的 API 和最小化的配置來實現(xiàn)高效的狀態(tài)管理。
基于 Hooks:它完全依賴于 React 的 Hooks 機制,允許開發(fā)者以聲明式的方式訂閱狀態(tài)變化并觸發(fā)更新。
無特定立場:Zustand 不強制任何特定的設計模式或結構,給予開發(fā)者最大的靈活性。
單一數(shù)據(jù)源:盡管 Zustand 支持多個獨立的 store,但每個 store 內(nèi)部仍然遵循單一數(shù)據(jù)源的原則,即所有狀態(tài)都集中存儲在一個地方。
模塊化狀態(tài)切片:狀態(tài)可以被分割成不同的切片(slices),每個切片負責一部分應用邏輯,便于管理和維護。
異步支持:Zustand 可以輕松處理異步操作,允許在 store 中定義異步函數(shù)來執(zhí)行如 API 請求等任務。
- 基本使用:
- 創(chuàng)建 Store:在 Zustand 中,Store是通過create函數(shù)創(chuàng)建的。每個Store都包含狀態(tài)和處理狀態(tài)的函數(shù)。
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0, // 初始狀態(tài)
increment: () => set((state) => ({ count: state.count + 1 })), // 增加count的函數(shù)
decrement: () => set((state) => ({ count: state.count - 1 })), // 減少count的函數(shù)
}));
其中, create函數(shù)接受一個回調函數(shù),該回調函數(shù)接受一個set函數(shù)作為參數(shù),用于更新狀態(tài)。在這個回調函數(shù)中,定義了一個count狀態(tài)和兩個更新函數(shù)increment和decrement。
2. 使用 Store:在組件中,可以使用自定義的 Hooks(上面的useStore)來獲取狀態(tài)和更新函數(shù),并在組件中使用它們。
import React from 'react';
import { useStore } from './store';
function Counter() {
const { count, increment, decrement } = useStore();
return (
<div>
<p>Count: {count}</p>
<button notallow={increment}>Increment</button>
<button notallow={decrement}>Decrement</button>
</div>
);
}
3. 訂閱特定狀態(tài)片段:如果有一個包含多個狀態(tài)的store,但在組件中只需要訂閱其中一個狀態(tài),可以通過解構賦值從useStore返回的完整狀態(tài)對象中提取需要的狀態(tài)。Zustand的智能選擇器功能允許這樣做,而不會導致不必要的重新渲染。
// store.js
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
name: 'Zustand Store',
increment: () => set((state) => ({ count: state.count + 1 })),
setName: (newName) => set({ name: newName }),
}));
export default useStore;
在組件中,如果只想訂閱count狀態(tài),可以這樣做:
// MyComponent.js
import React from 'react';
import useStore from './store';
function MyComponent() {
const { count } = useStore((state) => ({ count: state.count }));
return (
<div>
<p>Count: {count}</p>
</div>
);
}
export default MyComponent;
Jotai
- 定義: Jotai 是一個輕量級且靈活的 React 狀態(tài)管理庫,采用原子化狀態(tài)管理模型。它受到了 Recoil 的啟發(fā),旨在提供一種簡單而直觀的方式來管理 React 中的狀態(tài)。
- 核心思想:
- 原子化狀態(tài)管理:Jotai 使用原子作為狀態(tài)的基本單位。每個原子代表一個獨立的狀態(tài)片段,可以被多個組件共享和訪問。
- 組合性:通過組合 atoms 和選擇器,可以構建復雜的、依賴于其他狀態(tài)的狀態(tài)邏輯。這種組合性使得狀態(tài)管理更加模塊化和靈活。
- 細粒度依賴跟蹤:Jotai 內(nèi)置了高效的依賴跟蹤機制,只有當組件實際依賴的狀態(tài)發(fā)生變化時才會觸發(fā)重新渲染。
- 基本使用:
- 簡單原子創(chuàng)建:定義一個 atom 來表示應用中的某個狀態(tài)片段。
- 創(chuàng)建 Atom:
import { atom } from 'jotai';
export const countAtom = atom(0);
派生原子創(chuàng)建(基于已有原子進行計算等)
import { atom } from 'jotai';
import { countAtom } from './atoms';
export const doubleCountAtom = atom((get) => get(countAtom) * 2);
- 使用 Atom:
- 使用 useAtom Hook 獲取和更新原子狀態(tài)(適用于讀寫原子狀態(tài)場景):
import React from 'react';
import { useAtom } from 'jotai';
import { countAtom } from './atoms';
const Counter = () => {
const [count, setCount] = useAtom(countAtom);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount((prev) => prev + 1)}>Increment</button>
<button onClick={() => setCount((prev) => prev - 1)}>Decrement</button>
</div>
);
};
export default Counter;
- 使用 useAtomValue Hook 僅獲取原子狀態(tài)(適用于只讀場景):
import React from 'react';
import { useAtomValue } from 'jotai';
import { doubleCountAtom } from './derivedAtoms';
const DoubleCounter = () => {
const doubleCount = useAtomValue(doubleCountAtom);
return <p>Double Count: {doubleCount}</p>;
};
export default DoubleCounter;
- 使用 useSetAtom Hook 僅獲取更新原子狀態(tài)的函數(shù)(適用于只寫場景):
import React from 'react';
import { useAtomValue } from 'jotai';
import { countAtom } from './derivedAtoms';
const IncrementButtonComponent = () => {
const setCount = useSetAtom(countAtom);
return (
<button onClick={() => setCount((prevCount) => prevCount + 1)}>Increment</button>
);
};
服務器狀態(tài)
- 定義: 服務器狀態(tài)指的是應用與服務端交互相關的狀態(tài),包括從服務器獲取的數(shù)據(jù)(如 API 響應)以及請求的狀態(tài)(如加載中、完成、失敗等)。
- 重要性:
數(shù)據(jù)一致性:確保前端展示的數(shù)據(jù)是最新的,并且與服務器上的數(shù)據(jù)一致。
用戶體驗:提供即時反饋,比如加載指示器、錯誤消息等,以改善用戶的交互體驗。
緩存策略:合理地使用緩存可以減少不必要的網(wǎng)絡請求,提高性能并節(jié)省帶寬。
錯誤處理:優(yōu)雅地處理網(wǎng)絡故障或其他異常情況,保證應用的穩(wěn)定性和可靠性。
- 使用選擇:
- useState + useEffect:當只需要從服務器加載一次數(shù)據(jù),并且不需要復雜的緩存或重試機制時使用。
- 自定義 Hooks: 當多個組件中有相似的數(shù)據(jù)獲取模式,可以將這部分邏輯提取成一個自定義 Hook 來減少重復代碼。
- React Query:一個強大的工具,特別適用于需要全面數(shù)據(jù)獲取功能的大型應用。
- SWR:以其簡潔性和對實時性的支持而聞名,是那些追求快速集成和良好用戶體驗的應用的理想選擇。
useState + useEffect
在 React 中,管理服務器狀態(tài)的最常見模式是結合使用useState和useEffect Hook。
- useState:用于定義組件內(nèi)的狀態(tài)變量。這些狀態(tài)可以保存從服務器獲取的數(shù)據(jù)、加載標志(例如isLoading),以及可能發(fā)生的錯誤信息。
- useEffect:用來處理副作用,比如發(fā)起網(wǎng)絡請求。它可以在組件掛載或狀態(tài)變化時執(zhí)行特定的操作。在網(wǎng)絡請求開始前,將isLoading設為true,表明正在加載數(shù)據(jù)。當接收到服務器響應后,依據(jù)響應內(nèi)容更新相關狀態(tài)變量,并將isLoading設為false。如果請求過程中發(fā)生錯誤,還可以設置一個錯誤狀態(tài)變量來存儲錯誤信息。
import React, { useState, useEffect } from 'react';
function APP() {
// data保存服務器返回的數(shù)據(jù),loading表示是否正在加載,error保存可能發(fā)生的錯誤信息。
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
setData(result); // 更新數(shù)據(jù)狀態(tài)
} catch (err) {
setError(err.message); // 設置錯誤信息
} finally {
setLoading(false); // 請求完成后關閉加載狀態(tài)
}
}
useEffect(() => {
fetchData();
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{data.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
export default DataFetcher;
這種方式存在問題:
- 代碼冗余: 在多個組件中重復編寫用于獲取不同服務器數(shù)據(jù)的 useState 和 useEffect 代碼,不僅增加了開發(fā)成本,還使得代碼結構變得復雜,難以維護和擴展。這種冗余可以通過創(chuàng)建自定義 Hook 或采用更高級的狀態(tài)管理解決方案來有效緩解。
- 狀態(tài)一致性: 當多個組件依賴于相同的服務器數(shù)據(jù)時,很難保證它們之間的狀態(tài)一致性。例如,如果一個組件更新了數(shù)據(jù),另一個組件可能不會立即感知到,需要手動實現(xiàn)狀態(tài)同步的邏輯,可能會導致不同組件顯示的數(shù)據(jù)不一致。
- 缺乏高級特性: 基礎的狀態(tài)管理方式缺乏一些高級特性,如自動緩存、自動數(shù)據(jù)重新獲取以及樂觀更新等。對于復雜的數(shù)據(jù)更新場景,如部分更新或失效數(shù)據(jù)的重新獲取,開發(fā)者需要手動編寫大量的額外代碼來實現(xiàn)這些功能。
- 性能問題: 由于缺乏內(nèi)置的緩存機制,對于頻繁請求的數(shù)據(jù),每次組件重新渲染時都可能觸發(fā)新的請求,從而增加性能開銷。此外,依賴相同數(shù)據(jù)的多個組件無法共享數(shù)據(jù)緩存,導致網(wǎng)絡資源的浪費和不必要的請求開銷。
- 復雜的錯誤處理:在處理不同類型的錯誤(如網(wǎng)絡錯誤、服務器端錯誤、權限錯誤等)時,開發(fā)者需要進行手動區(qū)分和處理,這增加了錯誤處理的復雜性。同時,實現(xiàn)自動重試機制或提供用戶友好的錯誤提示也需要額外的復雜邏輯和代碼編寫。
自定義 Hooks
為了優(yōu)化 useState 和 useEffect 組合使用時所遇到的問題,可以使用自定義 Hook 來封裝常見的服務器請求操作。這里將以 ahooks 提供的的 useRequest 為例。
aHooks 是一個由螞蟻金服開發(fā)的 React Hooks 庫,它提供了一系列實用的自定義Hooks來簡化常見的開發(fā)任務。
useRequest 的特性如下:
- 自動請求/手動請求
- SWR(stale-while-revalidate)
- 緩存/預加載
- 屏幕聚焦重新請求
- 輪詢
- 防抖
- 節(jié)流
- 并行請求
- 依賴請求
- loading delay
- 分頁
- 加載更多,數(shù)據(jù)恢復 + 滾動位置恢復
import { useRequest } from 'ahooks';
import React from 'react';
import Mock from 'mockjs';
function getUsername(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(Mock.mock('@name'));
}, 1000);
});
}
export default () => {
const { data, loading, run, cancel } = useRequest(getUsername, {
pollingInterval: 1000,
pollingWhenHidden: false,
});
return (
<>
<p>Username: {loading ? 'loading' : data}</p>
<button type="button" onClick={run}>
start
</button>
<button type="button" onClick={cancel} style={{ marginLeft: 8 }}>
stop
</button>
</>
);
};
當然,我們也可以根據(jù)需求來完全自定義 Hooks。
React Query
React Query(現(xiàn)稱為 TanStack Query)是一個專為React應用設計的數(shù)據(jù)獲取、緩存和狀態(tài)管理庫。它通過簡化常見的數(shù)據(jù)操作任務,如發(fā)起HTTP請求、處理加載狀態(tài)、錯誤處理等,極大地提升了開發(fā)效率和用戶體驗。
圖片
React Query 的功能如下:
- 簡化數(shù)據(jù)獲?。嚎梢允褂?useQuery Hook 可以輕松發(fā)起網(wǎng)絡請求,自動處理加載狀態(tài)、錯誤處理,并返回響應數(shù)據(jù)。支持多種類型的請求,包括RESTful API 和 GraphQL。
- 內(nèi)置緩存機制:自動管理和優(yōu)化緩存,減少不必要的網(wǎng)絡請求。提供 cacheTime 和 staleTime 配置項來控制緩存的有效期和數(shù)據(jù)的新鮮度。
- 自動重新獲取數(shù)據(jù):支持在特定情況下自動刷新數(shù)據(jù),如窗口重新聚焦 (refetchOnWindowFocus) 或者網(wǎng)絡連接恢復 (refetchOnReconnect)。有助于確保用戶始終看到最新的數(shù)據(jù),特別是在長時間離開頁面后再返回時。
- 并發(fā)模式支持:確保在網(wǎng)絡請求未完成時安全地卸載組件,避免內(nèi)存泄漏和其他潛在問題。
- 樂觀更新與突變管理:通過 useMutation Hook 支持創(chuàng)建、更新或刪除資源的操作。實現(xiàn)樂觀更新,即先顯示預期的結果,如果請求失敗則回滾到原始狀態(tài)。
- 無限滾動與分頁:使用 useInfiniteQuery Hook 來實現(xiàn)無限滾動功能,簡化分頁邏輯的管理。
- 開發(fā)工具:提供 ReactQueryDevtools 開發(fā)工具,幫助開發(fā)者清晰地觀察每個請求的狀態(tài)及其相關緩存信息。
- 手動觸發(fā)請求:允許通過 manual 選項將請求設置為手動觸發(fā),配合 run 方法按需發(fā)起請求。
- 全局配置:可以通過 QueryClient 進行全局配置,如設置默認的緩存時間、重試策略等。
import {
useQuery,
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
import { getTodos, postTodo } from '../my-api'
const queryClient = new QueryClient()
function App() {
return (
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
)
}
function Todos() {
const queryClient = useQueryClient()
const query = useQuery({ queryKey: ['todos'], queryFn: getTodos })
const mutation = useMutation({
mutationFn: postTodo,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
return (
<div>
<ul>{query.data?.map((todo) => <li key={todo.id}>{todo.title}</li>)}</ul>
<button
onClick={() => {
mutation.mutate({
id: Date.now(),
title: 'Do Laundry',
})
}}
>
Add Todo
</button>
</div>
)
}
render(<App />, document.getElementById('root'))
SWR
SWR 是 "stale-while-revalidate" 的縮寫,它是一個用于數(shù)據(jù)獲取和緩存的React Hooks庫,由 Vercel 開發(fā)。SWR 專為構建快速響應的用戶界面而設計,它的工作原理是先返回緩存的數(shù)據(jù),然后在后臺發(fā)起請求獲取最新的數(shù)據(jù),并在收到新數(shù)據(jù)后更新UI。這種模式可以提供即時的用戶體驗,同時確保數(shù)據(jù)保持最新。
SWR 的特性如下:
- 即時響應:當組件首次渲染時,SWR 會立即返回緩存中的舊數(shù)據(jù)(如果有)。這使得用戶界面能夠瞬間加載,無需等待網(wǎng)絡請求完成。
- 自動重驗證:一旦從緩存中讀取了數(shù)據(jù),SWR 就會在后臺發(fā)起請求以獲取最新數(shù)據(jù)。這個過程對用戶來說是透明的,只有當新數(shù)據(jù)到達時才會更新UI。
- 簡單的API:SWR 提供了一個非常直觀的 useSWR Hook 來簡化數(shù)據(jù)獲取邏輯,包括處理加載狀態(tài)、錯誤和成功情況。
- 支持多種數(shù)據(jù)源:雖然 SWR 最常用于HTTP請求,但它也可以與其他類型的數(shù)據(jù)源一起工作,比如 WebSocket 或者本地存儲。
- 優(yōu)化性能:SWR 內(nèi)置了一些性能優(yōu)化特性,如并發(fā)模式支持、自動垃圾回收和去抖動/節(jié)流功能,幫助減少不必要的請求。
- 開發(fā)工具集成:與 React Query 類似,SWR 也提供了開發(fā)者工具來監(jiān)控和調試數(shù)據(jù)獲取行為。
- 靈活配置:可以通過配置選項自定義刷新策略、重試機制等,以適應不同的應用場景需求。
- 服務端渲染(SSR)兼容:SWR 支持服務器端渲染,可以在頁面初次加載時預先填充緩存,從而加快客戶端的首次渲染速度。
import React, { useState } from 'react';
import useSWR from 'swr';
import axios from 'axios';
// 自定義的 fetcher 函數(shù),使用 axios
const fetcher = (url) => axios.get(url).then((res) => res.data);
// 自定義的 SWR 配置選項
const swrConfig = {
// 重試次數(shù)
retry: 3,
// 緩存時間(毫秒)
revalidateOnFocus: false,
shouldRetryOnError: false,
};
function BlogPosts() {
const [page, setPage] = useState(1);
const perPage = 10;
// 構建 API URL,包含分頁參數(shù)
const apiUrl = `https://api.example.com/posts?page=${page}&perPage=${perPage}`;
// 使用 SWR 獲取文章數(shù)據(jù)
const { data: posts, error, isValidating } = useSWR(apiUrl, fetcher, swrConfig);
// 數(shù)據(jù)轉換:按日期排序
const sortedPosts = posts?.sort((a, b) => new Date(b.date) - new Date(a.date)) || [];
if (error) return <div>加載出錯: {error.message}</div>;
if (!posts) return <div>正在加載... {isValidating && '重新驗證...'}</div>;
return (
<div>
<h1>博客文章</h1>
<ul>
{sortedPosts.map((post) => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
<small>發(fā)布于: {new Date(post.date).toLocaleDateString()}</small>
</li>
))}
</ul>
<button onClick={() => setPage(page - 1)} disabled={page === 1}>
上一頁
</button>
<button onClick={() => setPage(page + 1)}>
下一頁
</button>
</div>
);
}
export default BlogPosts;
導航狀態(tài)
- 定義: React 導航狀態(tài)是指與應用內(nèi)部頁面或視圖之間的導航相關的狀態(tài)。它包括但不限于當前路由信息、歷史記錄棧、參數(shù)傳遞以及可能的其他元數(shù)據(jù)。當進行路由導航時,有時需要將前一個頁面的狀態(tài)帶到新頁面,以確保用戶體驗的一致性和連續(xù)性。通常,在 React 項目中會借助 React Router、TanStack Router 等路由庫來實現(xiàn)狀態(tài)管理。
React Router
- 定義: React Router 是 React 應用中最常用的路由庫,提供了豐富的路由功能,支持路由匹配、嵌套路由、路由參數(shù)提取、動態(tài)路由、重定向等??梢苑奖愕嘏c React 應用集成,實現(xiàn)頁面之間的導航和導航狀態(tài)的管理。
- 使用:在 React Router 中,可以通過路徑參數(shù)、查詢參數(shù)、狀態(tài)對象來實現(xiàn)狀態(tài)管理。
Link:創(chuàng)建一個導航鏈接,to="/product/123" 表示點擊該鏈接會跳轉到 /product/123 路徑。
Route:定義路由規(guī)則,path="/product/:productId" 是一個包含路徑參數(shù)的路由,productId 是參數(shù)名稱。
useParams:在 ProductDetail 組件中,通過 useParams 鉤子函數(shù)可以獲取到當前匹配路由的路徑參數(shù)。在這個例子中,它將從 URL 中提取 productId 并顯示在頁面上。
- 路徑參數(shù): 路徑參數(shù)通常用于在 URL 路徑中傳遞信息。
// 定義路由
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import UserDetail from './UserDetail';
function App() {
return (
<Router>
<Routes>
<Route path="/users/:userId" element={<UserDetail />} />
</Routes>
</Router>
);
}
export default App;
// 路由跳轉
<Link to="/users/123">用戶中心</Link>
// 使用參數(shù)
import { useParams } from 'react-router-dom';
function UserDetail() {
const { userId } = useParams();
return (
<div>
<h1>User Detail</h1>
<p>User ID: {userId}</p>
</div>
);
}
export default UserDetail;
- 查詢字符串:查詢字符串通常用于在 URL 查詢字符串中傳遞信息。
- useLocation:通過 useLocation 獲取當前頁面的位置信息,包括查詢參數(shù)。
- new URLSearchParams(location.search):將 location.search 部分(以 ? 開始的查詢字符串)解析為一個 URLSearchParams 對象。
- queryParams.get('keyword') 和 queryParams.get('category'):通過 get 方法從 URLSearchParams 對象中獲取相應的查詢參數(shù)。
// 定義路由
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import SearchResults from './SearchResults';
function App() {
return (
<Router>
<Routes>
<Route path="/search" element={<SearchResults />} />
</Routes>
</Router>
);
}
export default App;
// 路由跳轉
<Link to="/search?keyword=phone">搜索</Link>
// 使用參數(shù)
import { useLocation } from 'react-router-dom';
function SearchResults() {
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
return (
<div>
<h1>搜索結果</h1>
<p>關鍵詞: {queryParams.get('keyword') || 'None'}</p>
</div>
);
}
export default SearchResults;
- 狀態(tài)傳遞:狀態(tài)對象通常用于在導航時傳遞復雜的狀態(tài)信息。
- useNavigate 支持傳遞額外的狀態(tài)對象給目標頁面。這些狀態(tài)不會出現(xiàn)在 URL 中,但在目標頁面可以通過 useLocation Hook 獲取。
// 源頁面?zhèn)鬟f狀態(tài)
import { useNavigate } from 'react-router-dom';
function ProductList() {
const navigate = useNavigate();
const handleAddToCart = (productId) => {
navigate('/cart', {
state: { productId }
});
};
return (
<div>
<h1>商品列表</h1>
<button onClick={() => handleAddToCart('123')}>添加到購物車</button>
</div>
);
}
export default ProductList;
// 新頁面使用狀態(tài)
import { useLocation } from 'react-router-dom';
function Cart() {
const location = useLocation();
const { state } = location;
return (
<div>
<h1>購物車</h1>
{state ? (
<p>商品ID: {state.productId}</p>
) : (
<p>購物車為空</p>
)}
</div>
);
}
export default Cart;
TanStack Router
- 定義: Tanstack Router 是由 TanStack 團隊開發(fā)的一個用于 React 和其他框架的高性能路由庫。Tanstack Router 強調性能、可擴展性和易用性,支持多種渲染環(huán)境,包括客戶端、服務端和靜態(tài)站點生成。
- 特點:
動態(tài)路由匹配:支持復雜的路徑模式,例如參數(shù)化路徑、通配符等。
嵌套路由:可以定義父子關系的路由結構,適用于構建復雜的應用布局。
編程式導航:提供了類似 useNavigate 的 Hook 來執(zhí)行編程式導航。
狀態(tài)管理:內(nèi)置對 URL 狀態(tài)的支持,方便地將查詢參數(shù)和路徑參數(shù)傳遞給組件。
跨框架兼容:不僅限于 React,還支持 Vue、Solid 等其他前端框架。
性能優(yōu)化:通過懶加載(Lazy Loading)、代碼分割(Code Splitting)等技術來提高應用性能。
全面的功能集:
插件系統(tǒng):擁有豐富的插件生態(tài),可以輕松添加額外的功能,如身份驗證、緩存控制等。
服務端渲染(SSR)和靜態(tài)站點生成(SSG)支持:確保應用在不同渲染環(huán)境中都能良好運行。
類型安全:對于 TypeScript 用戶來說,Tanstack Router 提供了良好的類型定義和支持。
- 使用:
- 路徑參數(shù): 路徑參數(shù)是 URL 中固定位置的部分,用來標識特定資源或視圖。例如,/products/:id。在 TanStack Router 中,可以使用 useParams Hook 來獲取路徑參數(shù)。
// 定義路由
import { createRouter } from '@tanstack/react-router';
const router = createRouter({
routes: [
{
path: '/products/:id',
element: () => import('./pages/ProductDetail'),
},
],
});
export default router;
// 獲取路徑參數(shù)
import React from 'react';
import { useParams } from '@tanstack/react-router';
function ProductDetail() {
const params = useParams();
return <div>{params.id}</div>;
}
export default ProductDetail;
查詢字符串:查詢字符串是以問號 (?) 開始并由與號 (&) 分隔的一系列鍵值對,通常用于攜帶非層次結構的數(shù)據(jù),如搜索關鍵詞、排序選項、過濾條件等。在 TanStack Router 中,可以使用 useSearchParams Hook 來訪問和修改查詢字符串。
// 路由跳轉
import { Link } from '@tanstack/react-router';
function SearchLink() {
return (
<Link to="/search?q=laptop&sort=price_asc">搜索</Link>
);
}
// 獲取查詢參數(shù)
import React from 'react';
import { useSearchParams } from '@tanstack/react-router';
function SearchResults() {
const [searchParams] = useSearchParams();
const query = searchParams.get('q') || '';
const sort = searchParams.get('sort') || 'desc';
return (
<div>
<p>搜索: {query}, 分類: {sort}</p>
</div>
);
}
export default SearchResults;
- URL 中,但它們可以通過編程式導航來傳遞,并在目標頁面中通過 useLocation Hook 獲取。
// 傳遞狀態(tài)
import { useRouter } from '@tanstack/react-router';
function SubmitFormButton() {
const router = useRouter();
function handleSubmit() {
const formData = {
name: 'John Doe',
email: 'john.doe@example.com',
};
// Navigate to the next page and pass state
router.navigate('/confirmation', { state: formData });
}
return <button onClick={handleSubmit}>Submit Form</button>;
}
// 獲取狀態(tài)
import React from 'react';
import { useLocation } from '@tanstack/react-router';
function ConfirmationPage() {
const location = useLocation();
const { state } = location;
const { name, email } = state || {};
return (
<div>
<p>Name: {name}</p>
<p>Email: {email}</p>
</div>
);
}
export default ConfirmationPage;
表單狀態(tài)
- 定義: 在 React 中,表單狀態(tài)指的是用于保存和管理表單中各個輸入元素(如文本框、選擇框、復選框、單選按鈕等)的數(shù)據(jù)。這些數(shù)據(jù)反映了用戶與表單的交互情況,并且通常需要被收集起來以便后續(xù)處理,例如提交給服務器或用于本地邏輯計算。
- 重要性:
數(shù)據(jù)收集:從用戶那里獲取必要的信息,比如用戶的聯(lián)系信息、偏好設置或者訂單詳情。
驗證邏輯:確保用戶輸入的數(shù)據(jù)符合預期格式和規(guī)則,防止無效或惡意數(shù)據(jù)進入系統(tǒng)。
用戶體驗:提供即時反饋,比如顯示錯誤消息、動態(tài)更新選項或其他增強功能。
持久化:即使頁面刷新,也能夠保持未完成的表單內(nèi)容,避免用戶重新填寫。
- 使用選擇:
- 簡單表單:使用 useState,它足夠簡單且無需額外依賴。
- 中等復雜度表單:考慮使用 React Hook Form,特別是重視性能和希望保持依賴樹輕量化的時候。
- 復雜表單:選擇 Formik,它提供了最全面的功能集,非常適合構建大型、復雜的表單應用。
useState
這是最常用的方式,其中表單元素的值由 React 的狀態(tài)(useState)來控制。每當用戶更改輸入時,都會觸發(fā)一個事件處理器來更新狀態(tài),從而保持同步。
import React, { useState } from 'react';
function LoginForm() {
const [formData, setFormData] = useState({
username: '',
password: ''
});
const handleChange = (event) => {
const { name, value } = event.target;
setFormData(prevState => ({
...prevState,
[name]: value
}));
};
const handleSubmit = (event) => {
event.preventDefault();
console.log('表單數(shù)據(jù)', formData);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>用戶名:</label>
<input
type="text"
name="username"
value={formData.username}
onChange={handleChange}
/>
</div>
<div>
<label>密碼:</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
/>
</div>
<button type="submit">登錄</button>
</form>
);
}
export default LoginForm;
React Hook Form
- 定義: React Hook Form 是一個輕量級且高效的庫,它通過最小化不必要的渲染和優(yōu)化性能來構建表單。它的核心理念是讓開發(fā)者直接與 HTML 表單元素互動,而不是使用受控組件的方式。
- 使用:
- useForm Hook 創(chuàng)建了一個表單實例,并返回了一些有用的屬性和方法。
- register 方法用于注冊表單字段并添加驗證規(guī)則。
- handleSubmit 方法包裝了表單提交函數(shù),確保只有在所有驗證都通過的情況下才會調用實際的提交邏輯。
- errors 對象包含了各個字段的驗證錯誤信息,可以在 UI 中展示給用戶。
import React from 'react';
import { useForm } from 'react-hook-form';
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
const onSubmit = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>用戶名:</label>
<input
{...register('username', { required: '用戶名是必填項' })}
/>
{errors.username && <p>{errors.username.message}</p>}
</div>
<div>
<label>密碼:</label>
<input
type="password"
{...register('password', { required: '密碼是必填項' })}
/>
{errors.password && <p>{errors.password.message}</p>}
</div>
<button type="submit">登錄</button>
</form>
);
}
export default LoginForm;
Formik
- 定義: Formik 是另一個流行的表單管理庫,它提供了一套更全面的 API 來處理表單的狀態(tài)、驗證、提交等操作。Formik 的設計目標是讓開發(fā)者能夠快速地創(chuàng)建復雜的表單,而不需要編寫大量的樣板代碼。
- 使用:
- initialValues:設置初始表單值。
- validationSchema:定義驗證規(guī)則(這里使用了 Yup)。
- onSubmit:當表單成功提交時觸發(fā)的回調函數(shù)。
- Formik 組件包裹了整個表單,并接收三個主要屬性:
- Field 組件用于創(chuàng)建表單字段,它可以自動與 Formik 的內(nèi)部狀態(tài)同步。
- ErrorMessage 組件用于顯示特定字段的驗證錯誤消息。
- isSubmitting 狀態(tài)可以用來禁用提交按鈕,防止重復提交。
import React from 'react';
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
// 使用 Yup 定義驗證模式
const validationSchema = Yup.object().shape({
username: Yup.string()
.required('用戶名是必填項'),
password: Yup.string()
.required('密碼是必填項')
});
function LoginForm() {
return (
<Formik
initialValues={{ username: '', password: '' }} // 設置初始表單值
validationSchema={validationSchema} // 定義驗證規(guī)則
onSubmit={(values, actions) => {
console.log(values);
// 可選地,在這里執(zhí)行異步操作,并在完成后調用 actions.setSubmitting(false)
actions.setSubmitting(false);
}}
>
{({ isSubmitting }) => ( // 獲取提交狀態(tài)
<Form>
<div>
<label>用戶名:</label>
<Field name="username" type="text" /> {/* 創(chuàng)建用戶名輸入框 */}
<ErrorMessage name="username" component="div" /> {/* 顯示用戶名的錯誤信息 */}
</div>
<div>
<label>密碼:</label>
<Field name="password" type="password" /> {/* 創(chuàng)建密碼輸入框 */}
<ErrorMessage name="password" component="div" /> {/* 顯示密碼的錯誤信息 */}
</div>
<button type="submit" disabled={isSubmitting}> {/* 提交按鈕,當提交中時禁用 */}
登錄
</button>
</Form>
)}
</Formik>
);
}
export default LoginForm;
持久化狀態(tài)
- 定義: 在 React 應用中,持久化狀態(tài)是指將應用的狀態(tài)保存到一個持久的存儲介質中,以便在用戶關閉瀏覽器、刷新頁面或重新啟動應用后仍然能夠恢復這些狀態(tài)。持久化狀態(tài)對于提升用戶體驗非常重要,因為它可以避免用戶丟失數(shù)據(jù),并且能夠讓應用在不同會話之間保持一致的行為。
- 重要性:
- 數(shù)據(jù)保留:確保用戶輸入或選擇的數(shù)據(jù)不會因為頁面刷新或應用重啟而丟失。
- 用戶體驗:提供更流暢和連續(xù)的用戶體驗,減少重復操作。
- 離線支持:允許應用在沒有網(wǎng)絡連接的情況下繼續(xù)工作,并在網(wǎng)絡恢復時同步更改。
- 使用選擇:
- Web Storage:適合簡單的、短期或長期的客戶端狀態(tài)保存,尤其是用戶偏好設置。
- Cookies:主要用于處理認證和跨頁面通信,但要注意安全性和數(shù)據(jù)量限制。
- IndexedDB:一個強大的客戶端數(shù)據(jù)庫解決方案,適用于需要存儲大量數(shù)據(jù)或結構化數(shù)據(jù)的應用。
- Zustand 或 Redux 中間件:結合持久化插件是全局狀態(tài)管理和持久化的優(yōu)秀選擇,特別適合大型應用和復雜的狀態(tài)邏輯。
Web Storage
- localStorage:用于長期存儲數(shù)據(jù),即使瀏覽器關閉或設備重啟后仍然存在。適合保存用戶偏好設置、主題選項等不需要立即過期的信息。
- sessionStorage:僅在當前會話期間有效,當頁面關閉或瀏覽器標簽頁被關閉時數(shù)據(jù)會被清除。適合臨時性的狀態(tài),如表單輸入。
import React, { useState, useEffect } from 'react';
function PersistentForm() {
const [formData, setFormData] = useState(() => {
// 初始化狀態(tài)時從 sessionStorage 中讀取數(shù)據(jù)
return JSON.parse(sessionStorage.getItem('formData')) || {};
});
useEffect(() => {
// 每次狀態(tài)變化時更新 sessionStorage
sessionStorage.setItem('formData', JSON.stringify(formData));
}, [formData]);
const handleChange = (event) => {
const { name, value } = event.target;
setFormData((prevData) => ({
...prevData,
[name]: value,
}));
};
const handleSubmit = (event) => {
event.preventDefault();
console.log('提交表單:', formData);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>用戶名:</label>
<input
type="text"
name="username"
value={formData.username || ''}
onChange={handleChange}
/>
</div>
<div>
<label>密碼:</label>
<input
type="password"
name="password"
value={formData.password || ''}
onChange={handleChange}
/>
</div>
<button type="submit">登錄</button>
</form>
);
}
export default PersistentForm;
Cookies
雖然不常用作狀態(tài)管理工具,但 Cookies 可以用來存儲少量數(shù)據(jù)(通常不超過4KB),并且可以通過設置過期時間來控制數(shù)據(jù)的有效期限。需要注意的是,Cookies 會在每次 HTTP 請求中發(fā)送給服務器,因此不適合存儲大量數(shù)據(jù)或敏感信息。
IndexedDB
IndexedDB 是一種更強大的客戶端數(shù)據(jù)庫解決方案,適合存儲結構化的鍵值對數(shù)據(jù),適用于需要存儲大量數(shù)據(jù)的應用場景。它提供了事務處理能力,支持異步操作,并且比 localStorage 更加靈活。
狀態(tài)管理庫
如果已經(jīng)在項目中使用了 Redux 或 Zustand 這樣的全局狀態(tài)管理庫,那么可以通過集成相應的持久化插件來實現(xiàn)狀態(tài)的持久化。這些插件可以自動同步全局狀態(tài)與本地存儲之間的數(shù)據(jù)。
- Zustand:可以借助zustand/persist 中間件來實現(xiàn)狀態(tài)持久化。
import create from 'zustand'
import { persist } from '@zustand/middleware'
import { localStoragePersist } from 'zustand/persist'
const persistConfig = {
key: 'myStore', // 存儲在 localStorage 中的鍵名
storage: localStoragePersist, // 使用 localStorage 作為存儲介質
}
const createStore = () =>
create(
persist(
(set, get) => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
decrement: () => set(state => ({ count: state.count - 1 })),
})),
persistConfig
)
export const useStore = createStore
import React from 'react'
import { useStore } from './store'
const App = () => {
const { count, increment, decrement } = useStore()
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
)
}
export default App
- Redux: 可以借助redux-persist 庫來實現(xiàn)狀態(tài)持久化,redux-persist 是一個 Redux中間件,用于將Redux的狀態(tài)持久化到本地存儲中,并在應用重新加載時恢復狀態(tài)。
import { createStore } from 'redux';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage'; // 使用默認的localStorage
import rootReducer from './reducers'; // 引入根reducer
// 配置redux-persist
const persistConfig = {
key: 'root', // 保存數(shù)據(jù)時使用的鍵名
storage, // 存儲機制
};
// 創(chuàng)建持久化后的reducer
const persistedReducer = persistReducer(persistConfig, rootReducer);
// 創(chuàng)建Redux Store
const store = createStore(persistedReducer);
// 創(chuàng)建persistor對象,用于在應用中集成PersistGate
const persistor = persistStore(store);
export { store, persistor };
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { store, persistor } from './store'; // 引入剛才設置的store和persistor
import App from './App';
ReactDOM.render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>,
document.getElementById('root')
);