2025 React 狀態管理終極指南!
React 作為當下最受歡迎的前端框架,在構建復雜且交互豐富的應用時,狀態管理無疑是至關重要的一環。從簡單的本地狀態,到能讓多個組件協同工作的全局狀態,再到涉及服務器通信、導航切換、表單操作以及持久化存儲等不同場景下的狀態管理,每一個方面都影響著應用的性能、用戶體驗以及可維護性。本文將作為 React 狀態管理的全面指南,帶你深入了解這些不同類型狀態的管理方式與要點。
圖片
本地狀態
- 定義: 在 React 中,本地狀態是指組件內部管理的數據,這些數據可以影響組件的渲染輸出和行為。本地狀態是相對于全局狀態而言的,它只存在于單個組件中,用于存儲那些不需要在整個應用范圍內共享的信息。
- 特點:
- 私有性:本地狀態是特定于某個組件的,其他組件無法直接訪問或修改它。
- 局部性:狀態的變化只會影響該組件及其子組件,而不會影響到父組件或其他兄弟組件。
生命周期性:隨著組件的掛載、更新和卸載,本地狀態也會經歷相應的生命周期階段。
函數組件
useState
從 React 16.8 開始引入了 Hooks API,使得函數組件也可以擁有狀態。useState 是最常用的 Hook 之一,用來聲明和管理組件的狀態。
- 返回值:調用 useState 時,返回一個包含兩個元素的數組。第一個元素是當前的狀態值,可以在組件中直接使用來渲染 UI 等;第二個元素是一個函數,用于更新該狀態值,調用這個函數并傳入新的值后,React 會重新渲染組件,使 UI 根據新的狀態進行更新。
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>當前計數: {count}</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useReducer
對于那些涉及復雜狀態邏輯或狀態更新依賴于前一狀態的情況,useReducer 可能是一個更好的選擇。它類似于 Redux 的 reducer 函數,但只作用于單個組件內部。
- reducer 函數:它是整個狀態管理的核心邏輯部分,根據傳入的不同 action 類型,按照預先定義好的規則來計算并返回新的狀態,就像一個狀態處理的 “加工廠”,規范了狀態更新的流程。
- dispatch 函數:通過調用 dispatch 并傳入相應的 action 對象,可以觸發 reducer 函數執行,從而實現狀態的更新。這使得狀態更新的操作更加可預測、可維護,尤其是在復雜的狀態變化場景中優勢明顯。
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
可以創建自定義 Hook 來封裝特定的狀態邏輯,并且可以在多個組件之間共享這些邏輯。這不僅提高了代碼的復用性,還使得狀態管理更加模塊化。
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 和參數,以便在它們變化時重新獲取數據
return { data, isLoading, error };
};
export default useFetchData;
類組件
在 Hooks 出現之前,React 類組件通過定義 state 對象并在構造函數中初始化它來管理狀態。
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>當前計數: {this.state.count}</p>
<button onClick={this.increment}>
Click me
</button>
</div>
);
}
}
注意事項:
- 初始化:通常在類組件的構造函數 constructor 中通過給 this.state 賦值來初始化本地狀態,設定初始的狀態值。
- 更新狀態:使用 this.setState 方法來更新狀態,它有兩種常見的使用形式。一種是直接傳入一個包含新狀態值的對象,如 this.setState({ count: 10 });另一種是傳入一個函數,函數接收前一個狀態 prevState 作為參數,返回新的狀態對象,這種方式在基于前一狀態進行計算來更新狀態時非常有用,能避免一些由于異步操作等帶來的狀態更新問題。
全局狀態
- 定義: 在 React 中,全局狀態是指那些在整個應用中多個組件之間需要共享和訪問的狀態。與本地狀態不同,全局狀態不局限于單個組件,而是可以在應用的不同部分之間傳遞、更新和同步。
- 重要性: 當應用變得越來越大,組件之間的嵌套層次越來越深時,使用 props 逐層傳遞狀態(即“props drilling”)會變得非常麻煩且難以維護。全局狀態管理工具可以幫助解決這個問題,它們提供了一種集中管理和共享狀態的方式,減少了冗余代碼,并提高了開發效率。
- 使用選擇:
- 狀態提升: 當需要在兄弟組件中共享狀態時,可以將狀態提升到父組件。
- Context API:適合簡單的全局狀態管理,尤其是當需要避免 props drilling 時。
- Zustand:一個輕量級的選擇,適合小型到中型應用,特別是那些注重性能和易用性的項目。
- Jotai:為更細粒度的狀態管理提供了可能性,非常適合那些尋求模塊化和高效狀態管理的開發者。
狀態提升
- 定義: 狀態提升是 React 中一種常見的模式,用于將需要在多個組件之間共享的狀態移動到它們的最近公共父組件中進行管理。這種方法雖不是全局狀態管理的一部分,但它提供了一種方式來集中管理跨多個兄弟組件的狀態。
- 原理: 當兩個或更多的子組件需要訪問相同的數據時,可以將該數據及其相關的更新邏輯“提升”到這些子組件的共同祖先組件中。然后,通過 props 將數據和處理函數傳遞給需要它的子組件。
- 注意事項:
狀態更新的流向:狀態更新總是從父組件開始,父組件更新狀態后,將新的狀態作為 props 傳遞給子組件,觸發子組件的重新渲染。
避免過度提升:不要將所有狀態都盲目提升,只提升多個組件需要共享的狀態,以保持組件的簡潔和清晰。
Context API
- 定義: React 的 Context API 允許創建一個可以在組件樹中傳遞數據的上下文,使得不同層次的組件可以訪問這些數據,而無需通過 props 一層層傳遞。雖然 Context API 本身并不直接提供狀態管理功能,但它可以與 Hooks(如 useState)結合使用來實現簡單的全局狀態管理。
- 基本使用:
提供者: 在 React 19 之前,可以使用 MyContext.Provider的形式來提供共享狀態。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 提供的數據,實現對全局狀態的訪問和修改。
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 是一個輕量級且易于使用的狀態管理庫,專門為 React 應用設計。它的名字來源于德語單詞 "zustand",意為“狀態”。Zustand 的設計理念是提供一種簡單、直觀的方式來管理全局狀態,同時保持高性能和小體積。
- 特點:
簡潔性:Zustand 的設計理念是保持極簡主義,通過簡單的 API 和最小化的配置來實現高效的狀態管理。
基于 Hooks:它完全依賴于 React 的 Hooks 機制,允許開發者以聲明式的方式訂閱狀態變化并觸發更新。
無特定立場:Zustand 不強制任何特定的設計模式或結構,給予開發者最大的靈活性。
單一數據源:盡管 Zustand 支持多個獨立的 store,但每個 store 內部仍然遵循單一數據源的原則,即所有狀態都集中存儲在一個地方。
模塊化狀態切片:狀態可以被分割成不同的切片(slices),每個切片負責一部分應用邏輯,便于管理和維護。
異步支持:Zustand 可以輕松處理異步操作,允許在 store 中定義異步函數來執行如 API 請求等任務。
- 基本使用:
- 創建 Store:在 Zustand 中,Store是通過create函數創建的。每個Store都包含狀態和處理狀態的函數。
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0, // 初始狀態
increment: () => set((state) => ({ count: state.count + 1 })), // 增加count的函數
decrement: () => set((state) => ({ count: state.count - 1 })), // 減少count的函數
}));
其中, create函數接受一個回調函數,該回調函數接受一個set函數作為參數,用于更新狀態。在這個回調函數中,定義了一個count狀態和兩個更新函數increment和decrement。
2. 使用 Store:在組件中,可以使用自定義的 Hooks(上面的useStore)來獲取狀態和更新函數,并在組件中使用它們。
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. 訂閱特定狀態片段:如果有一個包含多個狀態的store,但在組件中只需要訂閱其中一個狀態,可以通過解構賦值從useStore返回的完整狀態對象中提取需要的狀態。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狀態,可以這樣做:
// 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 狀態管理庫,采用原子化狀態管理模型。它受到了 Recoil 的啟發,旨在提供一種簡單而直觀的方式來管理 React 中的狀態。
- 核心思想:
- 原子化狀態管理:Jotai 使用原子作為狀態的基本單位。每個原子代表一個獨立的狀態片段,可以被多個組件共享和訪問。
- 組合性:通過組合 atoms 和選擇器,可以構建復雜的、依賴于其他狀態的狀態邏輯。這種組合性使得狀態管理更加模塊化和靈活。
- 細粒度依賴跟蹤:Jotai 內置了高效的依賴跟蹤機制,只有當組件實際依賴的狀態發生變化時才會觸發重新渲染。
- 基本使用:
- 簡單原子創建:定義一個 atom 來表示應用中的某個狀態片段。
- 創建 Atom:
import { atom } from 'jotai';
export const countAtom = atom(0);
派生原子創建(基于已有原子進行計算等)
import { atom } from 'jotai';
import { countAtom } from './atoms';
export const doubleCountAtom = atom((get) => get(countAtom) * 2);
- 使用 Atom:
- 使用 useAtom Hook 獲取和更新原子狀態(適用于讀寫原子狀態場景):
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 僅獲取原子狀態(適用于只讀場景):
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 僅獲取更新原子狀態的函數(適用于只寫場景):
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>
);
};
服務器狀態
- 定義: 服務器狀態指的是應用與服務端交互相關的狀態,包括從服務器獲取的數據(如 API 響應)以及請求的狀態(如加載中、完成、失敗等)。
- 重要性:
數據一致性:確保前端展示的數據是最新的,并且與服務器上的數據一致。
用戶體驗:提供即時反饋,比如加載指示器、錯誤消息等,以改善用戶的交互體驗。
緩存策略:合理地使用緩存可以減少不必要的網絡請求,提高性能并節省帶寬。
錯誤處理:優雅地處理網絡故障或其他異常情況,保證應用的穩定性和可靠性。
- 使用選擇:
- useState + useEffect:當只需要從服務器加載一次數據,并且不需要復雜的緩存或重試機制時使用。
- 自定義 Hooks: 當多個組件中有相似的數據獲取模式,可以將這部分邏輯提取成一個自定義 Hook 來減少重復代碼。
- React Query:一個強大的工具,特別適用于需要全面數據獲取功能的大型應用。
- SWR:以其簡潔性和對實時性的支持而聞名,是那些追求快速集成和良好用戶體驗的應用的理想選擇。
useState + useEffect
在 React 中,管理服務器狀態的最常見模式是結合使用useState和useEffect Hook。
- useState:用于定義組件內的狀態變量。這些狀態可以保存從服務器獲取的數據、加載標志(例如isLoading),以及可能發生的錯誤信息。
- useEffect:用來處理副作用,比如發起網絡請求。它可以在組件掛載或狀態變化時執行特定的操作。在網絡請求開始前,將isLoading設為true,表明正在加載數據。當接收到服務器響應后,依據響應內容更新相關狀態變量,并將isLoading設為false。如果請求過程中發生錯誤,還可以設置一個錯誤狀態變量來存儲錯誤信息。
import React, { useState, useEffect } from 'react';
function APP() {
// data保存服務器返回的數據,loading表示是否正在加載,error保存可能發生的錯誤信息。
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); // 更新數據狀態
} catch (err) {
setError(err.message); // 設置錯誤信息
} finally {
setLoading(false); // 請求完成后關閉加載狀態
}
}
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;
這種方式存在問題:
- 代碼冗余: 在多個組件中重復編寫用于獲取不同服務器數據的 useState 和 useEffect 代碼,不僅增加了開發成本,還使得代碼結構變得復雜,難以維護和擴展。這種冗余可以通過創建自定義 Hook 或采用更高級的狀態管理解決方案來有效緩解。
- 狀態一致性: 當多個組件依賴于相同的服務器數據時,很難保證它們之間的狀態一致性。例如,如果一個組件更新了數據,另一個組件可能不會立即感知到,需要手動實現狀態同步的邏輯,可能會導致不同組件顯示的數據不一致。
- 缺乏高級特性: 基礎的狀態管理方式缺乏一些高級特性,如自動緩存、自動數據重新獲取以及樂觀更新等。對于復雜的數據更新場景,如部分更新或失效數據的重新獲取,開發者需要手動編寫大量的額外代碼來實現這些功能。
- 性能問題: 由于缺乏內置的緩存機制,對于頻繁請求的數據,每次組件重新渲染時都可能觸發新的請求,從而增加性能開銷。此外,依賴相同數據的多個組件無法共享數據緩存,導致網絡資源的浪費和不必要的請求開銷。
- 復雜的錯誤處理:在處理不同類型的錯誤(如網絡錯誤、服務器端錯誤、權限錯誤等)時,開發者需要進行手動區分和處理,這增加了錯誤處理的復雜性。同時,實現自動重試機制或提供用戶友好的錯誤提示也需要額外的復雜邏輯和代碼編寫。
自定義 Hooks
為了優化 useState 和 useEffect 組合使用時所遇到的問題,可以使用自定義 Hook 來封裝常見的服務器請求操作。這里將以 ahooks 提供的的 useRequest 為例。
aHooks 是一個由螞蟻金服開發的 React Hooks 庫,它提供了一系列實用的自定義Hooks來簡化常見的開發任務。
useRequest 的特性如下:
- 自動請求/手動請求
- SWR(stale-while-revalidate)
- 緩存/預加載
- 屏幕聚焦重新請求
- 輪詢
- 防抖
- 節流
- 并行請求
- 依賴請求
- loading delay
- 分頁
- 加載更多,數據恢復 + 滾動位置恢復
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>
</>
);
};
當然,我們也可以根據需求來完全自定義 Hooks。
React Query
React Query(現稱為 TanStack Query)是一個專為React應用設計的數據獲取、緩存和狀態管理庫。它通過簡化常見的數據操作任務,如發起HTTP請求、處理加載狀態、錯誤處理等,極大地提升了開發效率和用戶體驗。
圖片
React Query 的功能如下:
- 簡化數據獲取:可以使用 useQuery Hook 可以輕松發起網絡請求,自動處理加載狀態、錯誤處理,并返回響應數據。支持多種類型的請求,包括RESTful API 和 GraphQL。
- 內置緩存機制:自動管理和優化緩存,減少不必要的網絡請求。提供 cacheTime 和 staleTime 配置項來控制緩存的有效期和數據的新鮮度。
- 自動重新獲取數據:支持在特定情況下自動刷新數據,如窗口重新聚焦 (refetchOnWindowFocus) 或者網絡連接恢復 (refetchOnReconnect)。有助于確保用戶始終看到最新的數據,特別是在長時間離開頁面后再返回時。
- 并發模式支持:確保在網絡請求未完成時安全地卸載組件,避免內存泄漏和其他潛在問題。
- 樂觀更新與突變管理:通過 useMutation Hook 支持創建、更新或刪除資源的操作。實現樂觀更新,即先顯示預期的結果,如果請求失敗則回滾到原始狀態。
- 無限滾動與分頁:使用 useInfiniteQuery Hook 來實現無限滾動功能,簡化分頁邏輯的管理。
- 開發工具:提供 ReactQueryDevtools 開發工具,幫助開發者清晰地觀察每個請求的狀態及其相關緩存信息。
- 手動觸發請求:允許通過 manual 選項將請求設置為手動觸發,配合 run 方法按需發起請求。
- 全局配置:可以通過 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" 的縮寫,它是一個用于數據獲取和緩存的React Hooks庫,由 Vercel 開發。SWR 專為構建快速響應的用戶界面而設計,它的工作原理是先返回緩存的數據,然后在后臺發起請求獲取最新的數據,并在收到新數據后更新UI。這種模式可以提供即時的用戶體驗,同時確保數據保持最新。
SWR 的特性如下:
- 即時響應:當組件首次渲染時,SWR 會立即返回緩存中的舊數據(如果有)。這使得用戶界面能夠瞬間加載,無需等待網絡請求完成。
- 自動重驗證:一旦從緩存中讀取了數據,SWR 就會在后臺發起請求以獲取最新數據。這個過程對用戶來說是透明的,只有當新數據到達時才會更新UI。
- 簡單的API:SWR 提供了一個非常直觀的 useSWR Hook 來簡化數據獲取邏輯,包括處理加載狀態、錯誤和成功情況。
- 支持多種數據源:雖然 SWR 最常用于HTTP請求,但它也可以與其他類型的數據源一起工作,比如 WebSocket 或者本地存儲。
- 優化性能:SWR 內置了一些性能優化特性,如并發模式支持、自動垃圾回收和去抖動/節流功能,幫助減少不必要的請求。
- 開發工具集成:與 React Query 類似,SWR 也提供了開發者工具來監控和調試數據獲取行為。
- 靈活配置:可以通過配置選項自定義刷新策略、重試機制等,以適應不同的應用場景需求。
- 服務端渲染(SSR)兼容:SWR 支持服務器端渲染,可以在頁面初次加載時預先填充緩存,從而加快客戶端的首次渲染速度。
import React, { useState } from 'react';
import useSWR from 'swr';
import axios from 'axios';
// 自定義的 fetcher 函數,使用 axios
const fetcher = (url) => axios.get(url).then((res) => res.data);
// 自定義的 SWR 配置選項
const swrConfig = {
// 重試次數
retry: 3,
// 緩存時間(毫秒)
revalidateOnFocus: false,
shouldRetryOnError: false,
};
function BlogPosts() {
const [page, setPage] = useState(1);
const perPage = 10;
// 構建 API URL,包含分頁參數
const apiUrl = `https://api.example.com/posts?page=${page}&perPage=${perPage}`;
// 使用 SWR 獲取文章數據
const { data: posts, error, isValidating } = useSWR(apiUrl, fetcher, swrConfig);
// 數據轉換:按日期排序
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>發布于: {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;
導航狀態
- 定義: React 導航狀態是指與應用內部頁面或視圖之間的導航相關的狀態。它包括但不限于當前路由信息、歷史記錄棧、參數傳遞以及可能的其他元數據。當進行路由導航時,有時需要將前一個頁面的狀態帶到新頁面,以確保用戶體驗的一致性和連續性。通常,在 React 項目中會借助 React Router、TanStack Router 等路由庫來實現狀態管理。
React Router
- 定義: React Router 是 React 應用中最常用的路由庫,提供了豐富的路由功能,支持路由匹配、嵌套路由、路由參數提取、動態路由、重定向等。可以方便地與 React 應用集成,實現頁面之間的導航和導航狀態的管理。
- 使用:在 React Router 中,可以通過路徑參數、查詢參數、狀態對象來實現狀態管理。
Link:創建一個導航鏈接,to="/product/123" 表示點擊該鏈接會跳轉到 /product/123 路徑。
Route:定義路由規則,path="/product/:productId" 是一個包含路徑參數的路由,productId 是參數名稱。
useParams:在 ProductDetail 組件中,通過 useParams 鉤子函數可以獲取到當前匹配路由的路徑參數。在這個例子中,它將從 URL 中提取 productId 并顯示在頁面上。
- 路徑參數: 路徑參數通常用于在 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>
// 使用參數
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 獲取當前頁面的位置信息,包括查詢參數。
- new URLSearchParams(location.search):將 location.search 部分(以 ? 開始的查詢字符串)解析為一個 URLSearchParams 對象。
- queryParams.get('keyword') 和 queryParams.get('category'):通過 get 方法從 URLSearchParams 對象中獲取相應的查詢參數。
// 定義路由
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>
// 使用參數
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;
- 狀態傳遞:狀態對象通常用于在導航時傳遞復雜的狀態信息。
- useNavigate 支持傳遞額外的狀態對象給目標頁面。這些狀態不會出現在 URL 中,但在目標頁面可以通過 useLocation Hook 獲取。
// 源頁面傳遞狀態
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;
// 新頁面使用狀態
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 團隊開發的一個用于 React 和其他框架的高性能路由庫。Tanstack Router 強調性能、可擴展性和易用性,支持多種渲染環境,包括客戶端、服務端和靜態站點生成。
- 特點:
動態路由匹配:支持復雜的路徑模式,例如參數化路徑、通配符等。
嵌套路由:可以定義父子關系的路由結構,適用于構建復雜的應用布局。
編程式導航:提供了類似 useNavigate 的 Hook 來執行編程式導航。
狀態管理:內置對 URL 狀態的支持,方便地將查詢參數和路徑參數傳遞給組件。
跨框架兼容:不僅限于 React,還支持 Vue、Solid 等其他前端框架。
性能優化:通過懶加載(Lazy Loading)、代碼分割(Code Splitting)等技術來提高應用性能。
全面的功能集:
插件系統:擁有豐富的插件生態,可以輕松添加額外的功能,如身份驗證、緩存控制等。
服務端渲染(SSR)和靜態站點生成(SSG)支持:確保應用在不同渲染環境中都能良好運行。
類型安全:對于 TypeScript 用戶來說,Tanstack Router 提供了良好的類型定義和支持。
- 使用:
- 路徑參數: 路徑參數是 URL 中固定位置的部分,用來標識特定資源或視圖。例如,/products/:id。在 TanStack Router 中,可以使用 useParams Hook 來獲取路徑參數。
// 定義路由
import { createRouter } from '@tanstack/react-router';
const router = createRouter({
routes: [
{
path: '/products/:id',
element: () => import('./pages/ProductDetail'),
},
],
});
export default router;
// 獲取路徑參數
import React from 'react';
import { useParams } from '@tanstack/react-router';
function ProductDetail() {
const params = useParams();
return <div>{params.id}</div>;
}
export default ProductDetail;
查詢字符串:查詢字符串是以問號 (?) 開始并由與號 (&) 分隔的一系列鍵值對,通常用于攜帶非層次結構的數據,如搜索關鍵詞、排序選項、過濾條件等。在 TanStack Router 中,可以使用 useSearchParams Hook 來訪問和修改查詢字符串。
// 路由跳轉
import { Link } from '@tanstack/react-router';
function SearchLink() {
return (
<Link to="/search?q=laptop&sort=price_asc">搜索</Link>
);
}
// 獲取查詢參數
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 獲取。
// 傳遞狀態
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>;
}
// 獲取狀態
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;
表單狀態
- 定義: 在 React 中,表單狀態指的是用于保存和管理表單中各個輸入元素(如文本框、選擇框、復選框、單選按鈕等)的數據。這些數據反映了用戶與表單的交互情況,并且通常需要被收集起來以便后續處理,例如提交給服務器或用于本地邏輯計算。
- 重要性:
數據收集:從用戶那里獲取必要的信息,比如用戶的聯系信息、偏好設置或者訂單詳情。
驗證邏輯:確保用戶輸入的數據符合預期格式和規則,防止無效或惡意數據進入系統。
用戶體驗:提供即時反饋,比如顯示錯誤消息、動態更新選項或其他增強功能。
持久化:即使頁面刷新,也能夠保持未完成的表單內容,避免用戶重新填寫。
- 使用選擇:
- 簡單表單:使用 useState,它足夠簡單且無需額外依賴。
- 中等復雜度表單:考慮使用 React Hook Form,特別是重視性能和希望保持依賴樹輕量化的時候。
- 復雜表單:選擇 Formik,它提供了最全面的功能集,非常適合構建大型、復雜的表單應用。
useState
這是最常用的方式,其中表單元素的值由 React 的狀態(useState)來控制。每當用戶更改輸入時,都會觸發一個事件處理器來更新狀態,從而保持同步。
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('表單數據', 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 是一個輕量級且高效的庫,它通過最小化不必要的渲染和優化性能來構建表單。它的核心理念是讓開發者直接與 HTML 表單元素互動,而不是使用受控組件的方式。
- 使用:
- useForm Hook 創建了一個表單實例,并返回了一些有用的屬性和方法。
- register 方法用于注冊表單字段并添加驗證規則。
- handleSubmit 方法包裝了表單提交函數,確保只有在所有驗證都通過的情況下才會調用實際的提交邏輯。
- 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 來處理表單的狀態、驗證、提交等操作。Formik 的設計目標是讓開發者能夠快速地創建復雜的表單,而不需要編寫大量的樣板代碼。
- 使用:
- initialValues:設置初始表單值。
- validationSchema:定義驗證規則(這里使用了 Yup)。
- onSubmit:當表單成功提交時觸發的回調函數。
- Formik 組件包裹了整個表單,并接收三個主要屬性:
- Field 組件用于創建表單字段,它可以自動與 Formik 的內部狀態同步。
- ErrorMessage 組件用于顯示特定字段的驗證錯誤消息。
- isSubmitting 狀態可以用來禁用提交按鈕,防止重復提交。
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} // 定義驗證規則
onSubmit={(values, actions) => {
console.log(values);
// 可選地,在這里執行異步操作,并在完成后調用 actions.setSubmitting(false)
actions.setSubmitting(false);
}}
>
{({ isSubmitting }) => ( // 獲取提交狀態
<Form>
<div>
<label>用戶名:</label>
<Field name="username" type="text" /> {/* 創建用戶名輸入框 */}
<ErrorMessage name="username" component="div" /> {/* 顯示用戶名的錯誤信息 */}
</div>
<div>
<label>密碼:</label>
<Field name="password" type="password" /> {/* 創建密碼輸入框 */}
<ErrorMessage name="password" component="div" /> {/* 顯示密碼的錯誤信息 */}
</div>
<button type="submit" disabled={isSubmitting}> {/* 提交按鈕,當提交中時禁用 */}
登錄
</button>
</Form>
)}
</Formik>
);
}
export default LoginForm;
持久化狀態
- 定義: 在 React 應用中,持久化狀態是指將應用的狀態保存到一個持久的存儲介質中,以便在用戶關閉瀏覽器、刷新頁面或重新啟動應用后仍然能夠恢復這些狀態。持久化狀態對于提升用戶體驗非常重要,因為它可以避免用戶丟失數據,并且能夠讓應用在不同會話之間保持一致的行為。
- 重要性:
- 數據保留:確保用戶輸入或選擇的數據不會因為頁面刷新或應用重啟而丟失。
- 用戶體驗:提供更流暢和連續的用戶體驗,減少重復操作。
- 離線支持:允許應用在沒有網絡連接的情況下繼續工作,并在網絡恢復時同步更改。
- 使用選擇:
- Web Storage:適合簡單的、短期或長期的客戶端狀態保存,尤其是用戶偏好設置。
- Cookies:主要用于處理認證和跨頁面通信,但要注意安全性和數據量限制。
- IndexedDB:一個強大的客戶端數據庫解決方案,適用于需要存儲大量數據或結構化數據的應用。
- Zustand 或 Redux 中間件:結合持久化插件是全局狀態管理和持久化的優秀選擇,特別適合大型應用和復雜的狀態邏輯。
Web Storage
- localStorage:用于長期存儲數據,即使瀏覽器關閉或設備重啟后仍然存在。適合保存用戶偏好設置、主題選項等不需要立即過期的信息。
- sessionStorage:僅在當前會話期間有效,當頁面關閉或瀏覽器標簽頁被關閉時數據會被清除。適合臨時性的狀態,如表單輸入。
import React, { useState, useEffect } from 'react';
function PersistentForm() {
const [formData, setFormData] = useState(() => {
// 初始化狀態時從 sessionStorage 中讀取數據
return JSON.parse(sessionStorage.getItem('formData')) || {};
});
useEffect(() => {
// 每次狀態變化時更新 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
雖然不常用作狀態管理工具,但 Cookies 可以用來存儲少量數據(通常不超過4KB),并且可以通過設置過期時間來控制數據的有效期限。需要注意的是,Cookies 會在每次 HTTP 請求中發送給服務器,因此不適合存儲大量數據或敏感信息。
IndexedDB
IndexedDB 是一種更強大的客戶端數據庫解決方案,適合存儲結構化的鍵值對數據,適用于需要存儲大量數據的應用場景。它提供了事務處理能力,支持異步操作,并且比 localStorage 更加靈活。
狀態管理庫
如果已經在項目中使用了 Redux 或 Zustand 這樣的全局狀態管理庫,那么可以通過集成相應的持久化插件來實現狀態的持久化。這些插件可以自動同步全局狀態與本地存儲之間的數據。
- Zustand:可以借助zustand/persist 中間件來實現狀態持久化。
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 庫來實現狀態持久化,redux-persist 是一個 Redux中間件,用于將Redux的狀態持久化到本地存儲中,并在應用重新加載時恢復狀態。
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', // 保存數據時使用的鍵名
storage, // 存儲機制
};
// 創建持久化后的reducer
const persistedReducer = persistReducer(persistConfig, rootReducer);
// 創建Redux Store
const store = createStore(persistedReducer);
// 創建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')
);