用 useContext + useReducer 替代 redux

20,240

Redux 毫无疑问是众多 React 项目首选的状态管理方案,但我觉得 Redux 的开发体验并不好。

比如当你正在开发一个很复杂的功能,中途需要不断添加全局状态,每次添加都不得不重复如下步骤:

  1. 去到管理 redux 的文件夹,思考把这个状态放到状态树的哪个位置,然后新建一个文件夹并命名 myFeature
  2. 创建三个文件 my-feature/actions.jsmy-feature/reducer.jsmy-feature/type.js
  3. combineReducer 和并 reduce
  4. 将 action 引入到组件中
  5. 通过 connect HOC 与你的组件相连
  6. 增加两个方法 mapStateToProps 和 mapDispatchToProps

以上只是加个状态而已,写很多模板代码还是其次,最要命的是会打断你写代码的思路。

而且随着项目越来越大, redux 的状态树也会变大,维护也会变困难。

useContext + useReducer 如何替代 redux ?

useContextuseReducer 是 React 16.8 引入的新 API。

useContext:可访问全局状态,避免一层层的传递状态。

useReducer:用过 Redux 肯定不会陌生,它主要用于更新复杂逻辑的状态。

下面通过一个简单例子介绍 useContext + useReducer 是如何替代 Redux 的。

代码已放到 codesandbox,查看完整代码

这个例子只有一个功能,点击按钮改变字体颜色。

开始

首先用 create-react-app 创建一个项目,也可以在 CodeSandbox 上创建一个 React App。

创建颜色展示组件 ShowArea

import React from 'react'

const ShowArea = (props) => {
  return (
    <div style={{color: "blue"}}>字体颜色展示为blue</div>
  )
}

export default ShowArea

创建按钮组件 buttons

import React from "react";

const Buttons = props => {
  return (
    <React.Fragment>
      <button>红色</button>
      <button>黄色</button>
    </React.Fragment>
  );
};

export default Buttons;

将 ShowArea 和 Buttons 导入 index.js

import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";

import ShowArea from './ShowArea'
import Buttons from './Buttons'

function App() {
  return (
    <div className="App">
      <ShowArea />
      <Button />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

状态管理

很明显 ShowArea 组件需要一个颜色状态,所以我们创建 color.js 来处理状态。

// color.js
import React, { createContext } from "react";

// 创建 context
export const ColorContext = createContext({});

/**
 * 创建一个 Color 组件
 * Color 组件包裹的所有子组件都可以通过调用 ColorContext 访问到 value
 */
export const Color = props => {
  return (
    <ColorContext.Provider value={{ color: "blue" }}>
      {props.children}
    </ColorContext.Provider>
  );
};

引入状态

修改 index.js,让所有子组件都可以访问到颜色。

// index.js
···
···
···
import { Color } from "./color";

function App() {
  return (
    <div className="App">
      <Color>
        <ShowArea />
        <Buttons />
      </Color>
    </div>
  );
}
···
···
···

获取状态

在 ShowArea 组件中获取颜色

// ShowArea.js
···
···
···
import { ColorContext } from "./color";

const ShowArea = props => {
  const { color } = useContext(ColorContext);
  return <div style={{ color: color }}>字体颜色展示为{color}</div>;
};
···
···
···

创建 reducer

接着在 color.js 中添加 reducer, 用于处理颜色更新的逻辑。

import React, { createContext, useReducer } from "react";

// 创建 context
export const ColorContext = createContext({});

// reducer
export const UPDATE_COLOR = "UPDATE_COLOR"
const reducer = (state, action) => {
  switch(action.type) {
    case UPDATE_COLOR:
      return action.color
    default:
      return state  
  }
}

/**
 * 创建一个 Color 组件
 * Color 组件包裹的所有组件都可以访问到 value
 */
export const Color = props => {
  const [color, dispatch] = useReducer(reducer, 'blue')
  return (
    <ColorContext.Provider value={{color, dispatch}}>
      {props.children}
    </ColorContext.Provider>
  );
};

更新状态

为按钮添加点击事件,调用 dispatch 就可以更新颜色了。

// buttons.js

import React, { useContext } from "react";
import { colorContext, UPDATE_COLOR } from "./color";

const Buttons = props => {
  const { dispatch } = useContext(colorContext);
  return (
    <React.Fragment>
      <button
        onClick={() => {
          dispatch({ type: UPDATE_COLOR, color: "red" });
        }}
      >
        红色
      </button>
      <button
        onClick={() => {
          dispatch({ type: UPDATE_COLOR, color: "yellow" });
        }}
      >
        黄色
      </button>
    </React.Fragment>
  );
};

export default Buttons;

总结

  • useContext 创建全局状态,不用一层一层的传递状态。
  • useReducer 创建 reducer 根据不同的 dispatch 更新 state。
  • 代码写到哪里状态就加到哪里,不用打断思路跳到 redux 里面去写。
  • 全局状态分离,避免项目变大导致 Redux 状态树难以管理。

所以 useContext + useReducer 完全可以替代 React 进行状态管理。但是目前绝大多数 React 项目仍在使用 Redux,所以深入学习 Redux 还是很有必要的。

参考