[React Hooks长文总结系列三]为所欲为,制作“穷人版”的redux

react

前言

在离职之后,我开始静下心来,思考原来在繁重的业务开发节奏中无暇思考的一些问题,本期的主题是纯函数钩子useReducer和共享状态钩子useContext

什么是reducer函数?

react中,reducer函数是一个很重要的概念。它表示一个接收旧状态,返回新状态的函数。

const nums = [1, 2, 3]
const value = nums.reduce((acc, cur) => acc + cur, 0)

在上述例子中,reduce函数的一个参数,就是一个标准的reducer函数。

在之前的setState使用中,你可能会好奇setNum(prev => prev + 1)prev怎么来的,让我们深入到最底层看看吧,实际上useState并非最底层的元素,它内部仍然用到了useReducer来实现,在react源码中有个basicStateReducer,大致结构如下:

function basicStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action;
}

所以,当我们的setter接收的参数是一个函数时,旧的state将作为参数被该函数使用。

useReducer

useReducer基本用法如下:

const [state, dispatch] = useReducer(reducer, initialState, initFunc);

其中第三个参数是可选参数,我们一般只会用到前两个。

一个简单的示例(实现数字+1)如下:

import React, { useReducer } from "react";

const initialState = {
  num: 0,
};

function reducer(state, action) {
  switch (action.type) {
    case "increase":
      return { num: state.num + 1 };
    case "decrease":
      return { num: state.num - 1 };
    default:
      throw new Error();
  }
}

const NumsDemo = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      <h1>数字为: {state.num}</h1>
      <button onClick={() => dispatch({ type: "increase" })}>+</button>
      <button onClick={() => dispatch({ type: "decrease" })}>-</button>
    </>
  );
};

export default NumsDemo;

你可能也发现了,useReduceruseState 非常类似:定义状态和修改状态的逻辑。useReducer 用起来更加麻烦,但是如果需要维护的状态本身比较复杂,多个状态之间相互依赖,那么 useReducer的好处才真正显示出来了:用一个语义化的action来隐藏修改状态的复杂细节。

useContext

useContext则比较简单,它用于在局部组件树中共享数据,有点类似于vue中的provide/inject,基本使用如下:

// 在模块的入口文件中定义
// Home.tsx
export const MyContext = React.createContext({num: 24});

// 在模块内部的某个组件中获取
// Sub.tsx
import MyContext from '../Home';

const Sub = () => {
  const state = useContext(MyContext);
  // ...
}

redux的基本理念以及解决了什么问题

好了,现在我们已经确定要弄一个小型redux了,不过在这之前我们还是需要回顾一下redux的基本理念。

在对于小型页面共享数据时,我们一般会有诸如“状态提升”这样的开发约定,也就是说,我们会将共享的状态放到上层最近的公共父级。但是当页面数量一多,组件拆分粒度变细,这种“共享状态”的机制变得很脆弱,很冗余。

redux 的机制就是为了解决这个问题,redux 有几个非常明显的特点:

  1. 数据的唯一真相来源;
  2. reducer 纯函数;
  3. 单项数据流。

redux 的单项数据流,可以概括为三个部分:ViewReducersStore

View视图层发起更新动作(dispatch),会抵达更新函数层(Reducers),更新函数执行并返回最新状态,抵达状态层(Store),状态层更新后将通知视图层更新。

redux

其实我觉得,无论是 vuex 也好,redux 也好,它的设计理念都是类似一个“前端数据库”。在store层应该只存放公共状态,不建议存放其他的东西,比如公共方法,因为这与reducer纯函数的理念是相悖的。

实现一个简单的小型redux

好了,让一切开始吧,我们这里只定义三个组件:根组件App,第一个子组件Sub1,第二个子组件Sub2。实现一个非常简单的数字加减功能,如下:

// 说明:为了代码更加易读,已经将ts的类型定义做了删除操作
// App.tsx
import React, { useReducer } from "react";
import Sub1 from "./Sub1";
import Sub2 from "./Sub2";

const INITIAL_STATE = {
  name: "zhang",
  age: 24,
};

// 定义改变状态的几种操作
function reducer(state, action) {
  switch (action.type) {
    case "increaseAge":
      return { ...state, age: state.age + 1 };
    case "decreaseAge":
      return { ...state, age: state.age - 1 };
    case "changeName":
      return { ...state, name: action.payload };
    default:
      return state;
  }
}

// 选择性导出该context
export const AppContext = React.createContext(null);

function App() {
  const [state, dispatch] = useReducer(reducer, INITIAL_STATE);

  return (
    <AppContext.Provider value={{ state, dispatch }}>
      <Sub1 />
      <Sub2 />
    </AppContext.Provider>
  );
}

export default App;


// 在Sub1.tsx中
import React, { useContext } from "react";
import { AppContext } from "./App";

const Sub1 = () => {
  const { state, dispatch } = useContext(AppContext);
  return (
    <>
      <h1>Sub1年龄为: {state.age}, 名字为:{state.name}</h1>
      <button onClick={() => dispatch({ type: "increaseAge" })}>+</button>
    </>
  );
};

export default Sub1;

// 在Sub2.tsx中
import React, { useContext } from "react";
import { AppContext } from "./App";

const Sub2 = () => {
  const { state, dispatch } = useContext(AppContext);
  return (
    <>
      <h1>Sub2年龄为: {state.age}, 名字为:{state.name}</h1>
      <button onClick={() => dispatch({ type: "decreaseAge" })}>-</button>
    </>
  );
};

export default Sub2;

运行以上示例,可以发现在一处子组件更改公共状态,其他消费到该公共状态的组件(Consumer)都会更新。这有效避免了隔代传props所引发的代码臃肿脆弱问题。

useReducer + useContext 能否代替 redux?

不能,我在项目中虽然已经这么用了,但是还是发现对比redux的功能是有所欠缺的,其中最典型的就是更新公共状态后没有回调的问题。

useReducer + useContext实际上是制造了一个“穷人版的 redux”。而且我们必须花费额外的心思去避免性能问题(React.memouseCallback等),然而这些烦人的 dirty works 其实 React-Redux 等工具已经默默替我们解决了。除此之外,我们还会面临以下问题:

  • 需要自行实现 combineReducers 等辅助功能
  • 失去 Redux 生态的中间件支持
  • 失去 Redux DevTools 等调试工具
  • 出了坑不利于求助……

以上四个坑点摘抄于腾讯的这篇文章,仔细读完后发现确实写的可以:Redux with Hooks

以上是 [React Hooks长文总结系列三]为所欲为,制作“穷人版”的redux 的全部内容, 来源链接: utcz.com/z/383678.html

回到顶部