「2024」React 状态管理入门

概念

简单来说,状态指的是某一时刻应用中的数据或界面的呈现。这些数据可能包括用户填写表单的信息、应用内的用户偏好设置、应用的页面/路由状态、或者任何其他可能改变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 功能。

示例:

  1. 首先创建一个 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>
  );
};
  1. 接下来在组件树中使用 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;
  1. 最后,在需要访问状态的子组件中,使用 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 的组件都将重新渲染,不管它们是否真的依赖于这个变化的部分。在组件树较大且更新频繁的情况下,这可能会导致不必要的渲染,并对性能造成负担。

针对性能问题的优化策略:

  1. 拆分 Context:如果你的 Context 对象非常庞大,并且有不同的部分被不同的组件使用,那么拆分 Context 是一个很好的方式。通过将状态拆分为更小的、独立的 Contexts,组件可以订阅它们实际所需要的那一部分状态,从而减少不必要的渲染。
  2. 优化子组件:
  • 使用 React.memo:它是 React 提供的一个高阶组件,它会对组件的 props 进行浅比较,这样只有当组件的 props 发生变化时,组件才会重新渲染。
  1. 使用多个状态提供者:在大型应用中,可以在多个层级上设置提供者,而不是仅仅在应用的顶层。这样可以控制渲染发生的具体位置和范围。
  2. 状态选择:传递一个函数给 Context 消费者,而不是直接传递整个状态对象。这个函数可以从全局状态中选择组件特定需要的部分状态。这样不仅可以避免组件不必要的更新,同时还能使组件的意图更加明显。
  3. 避免在 Context Value 中传递一个新的对象或者函数:因为这会在每次 Provider 渲染时创建新的引用,导致所有消费者组件重新渲染。你可以通过使用 useCallback 来缓存函数,以及使用 useMemo 来缓存计算得出的值。
  4. 拆分状态和更新函数:有时你可能有一个大的状态对象,并且更新函数不经常变化。你可以创建两个 Contexts ------ 一个只传递状态,另一个只传递更新函数 ------ 这样当更新函数不变时,依赖状态的组件就不会在更新函数变化时重新渲染。
  5. 用状态管理库:如果你发现 Context 不适合你的应用需求或者你需要更细粒度的控制,可能需要使用如 Redux、MobX 或 Recoil 这样的状态管理库。

Redux

redux是GitHub star数和周下载量都最多的状态管理工具。

使用示例:

  1. 安装必要的包
bash 复制代码
npm install @reduxjs/toolkit react-redux

或者如果你在用 yarn:

bash 复制代码
yarn add @reduxjs/toolkit react-redux
  1. 创建 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;
  1. 设置 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')
);
  1. 访问状态和调用动作

在组件中使用 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 值,并且使用按钮来调用动作 incrementeddecrementedincrementedByAmount 来更新状态。

这只是使用 Redux 进行状态管理的一个简单示例,根据实际项目的复杂性,你可能需要更多的 reducers、middewares、selectors 或其他逻辑。

优缺点

优点:可扩展性高 & 繁荣的社区

缺点:大量的模版代码 & 状态量大起来后,有可能会出现性能问题

(要是都往redux里存,可想而知,每次action过来把所有reducer跑一遍。虽然 Redux后面开始支持拆分 store,异步加载 store,没到这个业务的场景的时候不加载这个业务的 store。但是如果业务耦合较为严重,那还是跑不掉)

Zustand

Zustand 是一个简单的、快速的状态管理解决方案,它不局限于 React 组件的层次结构,这就意味着你可以在任何地方访问状态,而无需使用 Provider 包装你的应用。

使用示例:

  1. 安装 Zustand

首先,你需要安装 Zustand。你可以通过 npm 或 yarn 来安装它:

bash 复制代码
npm install zustand

或者

bash 复制代码
yarn add zustand
  1. 创建一个 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;
  1. 在组件中使用 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

相关推荐
y先森19 分钟前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy19 分钟前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu108301891122 分钟前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿1 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡2 小时前
commitlint校验git提交信息
前端
Jacky(易小天)3 小时前
MongoDB比较查询操作符中英对照表及实例详解
数据库·mongodb·typescript·比较操作符
虾球xz3 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇3 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒3 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员3 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js