概念
简单来说,状态指的是某一时刻应用中的数据或界面的呈现。这些数据可能包括用户填写表单的信息、应用内的用户偏好设置、应用的页面/路由状态、或者任何其他可能改变UI的信息。
状态管理是前端开发中处理用户界面(UI)状态的过程,在复杂应用中尤其重要。随着应用规模的增长,管理不同组件和模块之间的状态变得越来越复杂。
在没有状态管理的情况下,应用组件通常需要进行大量的props传递(即将数据从一个组件传递到另一个组件),或者使用事件来通信,这在小型或简单的应用中是可行的。但在大型或复杂的项目中,这些方法难以维护和跟踪状态的变化,也会使得组件间耦合度增加,随之而来的问题包括难以追踪状态的变化源头和状态更新的影响。
为了解决这个问题,出现了各种状态管理库/模式,它们帮助开发者集中管理状态、提供更可预测的状态更新机制,并通过某种形式的全局状态存储,实现不同组件间的通信,而无需直接相互引用。比较知名的状态管理库包括 Redux、MobX、Zustand 等,各自有不同的实现原理和适用场景。例如,使用Redux的React应用会有一个单一的全局状态对象(store),所有的状态变化都通过一套严格的流程(actions -> reducers -> store)来管理,而React组件通过连接(connect)到这个全局状态来获取自己所需的状态部分,同时也可以触发状态的更新。这样,状态的变化就变得可追踪且可预测,而组件之间的关系也变得更为清晰。
状态管理的核心概念就是提高状态的可管理性,降低组件间的耦合度,并增强大型应用的可维护性。
状态管理工具介绍
目前实现状态管理的方式大概有下面几种:
- Context API
- Redux
- Zustand
- Mobx
- Recoil
- Jotai
- ...
Context API
Context 是 React 内置的状态管理工具,使用 Context 提供的 useContext + useReducer 可以实现一个基本的 Redux 功能。
示例:
- 首先创建一个 Context
typescript
import React, { createContext, useState, ReactNode } from 'react';
// 定义 context 的类型
interface IContext {
state: string;
updateState: (newState: string) => void;
}
// 创建一个 Context 对象, 初始值为 undefined
export const MyContext = createContext<IContext | undefined>(undefined);
interface IProviderProps {
children: ReactNode;
}
// 创建 Provider 组件
export const MyProvider: React.FC<IProviderProps> = ({ children }) => {
const [state, setState] = useState<string>('初始状态');
const updateState = (newState: string) => {
setState(newState);
};
return (
<MyContext.Provider value={{ state, updateState }}>
{children}
</MyContext.Provider>
);
};
- 接下来在组件树中使用 MyProvider 来包裹顶层组件
typescript
import React from 'react';
import { MyProvider } from './MyContext';
import ChildComponent from './ChildComponent';
const App: React.FC = () => {
return (
<MyProvider>
<ChildComponent />
</MyProvider>
);
};
export default App;
- 最后,在需要访问状态的子组件中,使用 useContext Hook:
typescript
import React, { useContext } from 'react';
import { MyContext } from './MyContext';
const ChildComponent: React.FC = () => {
const context = useContext(MyContext);
if (!context) {
throw new Error('useContext must be inside a Provider with a value');
}
const { state, updateState } = context;
const handleChange = () => {
updateState('更新后的状态');
};
return (
<div>
<p>{state}</p>
<button onClick={handleChange}>更改状态</button>
</div>
);
};
export default ChildComponent;
优缺点
优点:
- 作为 React 内置的 Hook,不需要引入第三方库,使用起来较为方便
缺点: - Context 只能存储单一的值,当数据量大起来的时候,需要创建大量的 Context。
- 使用 React Context 的一个已知的性能问题是,当一个 Context 的值发生变化时,所有消费该 Context 的组件都将重新渲染,不管它们是否真的依赖于这个变化的部分。在组件树较大且更新频繁的情况下,这可能会导致不必要的渲染,并对性能造成负担。
针对性能问题的优化策略:
- 拆分 Context:如果你的 Context 对象非常庞大,并且有不同的部分被不同的组件使用,那么拆分 Context 是一个很好的方式。通过将状态拆分为更小的、独立的 Contexts,组件可以订阅它们实际所需要的那一部分状态,从而减少不必要的渲染。
- 优化子组件:
- 使用 React.memo:它是 React 提供的一个高阶组件,它会对组件的 props 进行浅比较,这样只有当组件的 props 发生变化时,组件才会重新渲染。
- 使用多个状态提供者:在大型应用中,可以在多个层级上设置提供者,而不是仅仅在应用的顶层。这样可以控制渲染发生的具体位置和范围。
- 状态选择:传递一个函数给 Context 消费者,而不是直接传递整个状态对象。这个函数可以从全局状态中选择组件特定需要的部分状态。这样不仅可以避免组件不必要的更新,同时还能使组件的意图更加明显。
- 避免在 Context Value 中传递一个新的对象或者函数:因为这会在每次 Provider 渲染时创建新的引用,导致所有消费者组件重新渲染。你可以通过使用 useCallback 来缓存函数,以及使用 useMemo 来缓存计算得出的值。
- 拆分状态和更新函数:有时你可能有一个大的状态对象,并且更新函数不经常变化。你可以创建两个 Contexts ------ 一个只传递状态,另一个只传递更新函数 ------ 这样当更新函数不变时,依赖状态的组件就不会在更新函数变化时重新渲染。
- 用状态管理库:如果你发现 Context 不适合你的应用需求或者你需要更细粒度的控制,可能需要使用如 Redux、MobX 或 Recoil 这样的状态管理库。
Redux
redux是GitHub star数和周下载量都最多的状态管理工具。
使用示例:
- 安装必要的包:
bash
npm install @reduxjs/toolkit react-redux
或者如果你在用 yarn:
bash
yarn add @reduxjs/toolkit react-redux
- 创建 Redux 状态和动作:
首先,定义应用的状态和动作。在 store.ts
文件中,使用 createSlice
从 @reduxjs/toolkit
创建一个 slice,包含了状态的初识值、reducer 和自动生成的动作。
typescript
// store.ts
import { configureStore, createSlice, PayloadAction } from '@reduxjs/toolkit';
// 定义状态的类型
interface CounterState {
value: number;
}
// 初始状态
const initialState: CounterState = {
value: 0,
};
// 创建 slice
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
// 定义 reducer 和对应的动作
incremented: state => {
state.value += 1;
},
decremented: state => {
state.value -= 1;
},
incrementedByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
export const { incremented, decremented, incrementedByAmount } = counterSlice.actions;
// 配置 store
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export default store;
- 设置 Provider:
接下来,在应用的根组件中使用 Provider
包装应用,以便可以在组件树中的任意位置访问 Redux store。
typescript
// index.tsx 或 App.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
- 访问状态和调用动作:
在组件中使用 useSelector
从 Redux store 选择状态,并使用 useDispatch
发送动作。
typescript
// Counter.tsx
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { RootState, AppDispatch } from './store';
import { incremented, decremented, incrementedByAmount } from './store';
const Counter: React.FC = () => {
const count = useSelector((state: RootState) => state.counter.value);
const dispatch = useDispatch<AppDispatch>();
return (
<div>
<div>{count}</div>
<button onClick={() => dispatch(incremented())}>Increment</button>
<button onClick={() => dispatch(decremented())}>Decrement</button>
<button onClick={() => dispatch(incrementedByAmount(5))}>Increment by 5</button>
</div>
);
};
export default Counter;
在这个示例中,Counter
组件通过 useSelector
读取 Redux store 中的 counter
值,并且使用按钮来调用动作 incremented
、decremented
和 incrementedByAmount
来更新状态。
这只是使用 Redux 进行状态管理的一个简单示例,根据实际项目的复杂性,你可能需要更多的 reducers、middewares、selectors 或其他逻辑。
优缺点
优点:可扩展性高 & 繁荣的社区
缺点:大量的模版代码 & 状态量大起来后,有可能会出现性能问题
(要是都往redux里存,可想而知,每次action过来把所有reducer跑一遍。虽然 Redux后面开始支持拆分 store,异步加载 store,没到这个业务的场景的时候不加载这个业务的 store。但是如果业务耦合较为严重,那还是跑不掉)
Zustand
Zustand 是一个简单的、快速的状态管理解决方案,它不局限于 React 组件的层次结构,这就意味着你可以在任何地方访问状态,而无需使用 Provider 包装你的应用。
使用示例:
- 安装 Zustand:
首先,你需要安装 Zustand。你可以通过 npm 或 yarn 来安装它:
bash
npm install zustand
或者
bash
yarn add zustand
- 创建一个 store:
你可以创建一个新的文件,例如 useStore.ts
,并在其中定义你的状态和行为:
typescript
// useStore.ts
import create from 'zustand'
// 定义状态和动作的类型
interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
// 使用 create 创建一个 zustand store
const useStore = create<CounterState>(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
decrement: () => set(state => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
export default useStore;
- 在组件中使用 store:
然后你可以在组件中使用这个状态,如下所示:
typescript
// Counter.tsx
import React from 'react';
import useStore from './useStore';
const Counter: React.FC = () => {
// 使用 store 中的状态和行为
const { count, increment, decrement, reset } = useStore();
return (
<div>
<h2>Count: {count}</h2>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
};
export default Counter;
在这个例子中,Counter
组件使用了从 useStore
挂钩返回的 count
状态和三个更新这个状态的方法:increment
, decrement
, 和 reset
。
使用 Zustand,你不需要担心传统 Redux 所有的模板代码或 Context API 的潜在性能问题。 Zustand 提供了一个更灵活和轻量级的状态管理方案,特别适合在简单或中等复杂度的 React 应用中使用。
其他工具 TODO