「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

相关推荐
谢道韫6666 分钟前
今日总结 2024-12-24
javascript·vue.js·elementui
一朵好运莲7 分钟前
React引入Echart水球图
开发语言·javascript·ecmascript
米奇妙妙wuu13 分钟前
react使用sse流实现chat大模型问答,补充css样式
前端·css·react.js
傻小胖17 分钟前
React 生命周期完整指南
前端·react.js
梦境之冢1 小时前
axios 常见的content-type、responseType有哪些?
前端·javascript·http
racerun1 小时前
vue VueResource & axios
前端·javascript·vue.js
J总裁的小芒果1 小时前
THREE.js 入门(六) 纹理、uv坐标
开发语言·javascript·uv
m0_548514771 小时前
前端Pako.js 压缩解压库 与 Java 的 zlib 压缩与解压 的互通实现
java·前端·javascript
AndrewPerfect1 小时前
xss csrf怎么预防?
前端·xss·csrf
Calm5501 小时前
Vue3:uv-upload图片上传
前端·vue.js