再聊 Reducer Context 和 Redux

原文链接

这是一次突发奇想的感悟,感觉还挺神奇的,遂记录一下。

前言

作为一个React的开发者已经蛮久的了,大大小小的应用也开发了不少,除了一开始学习React时用过Redux以外,后来基本都不碰了,不管多么复杂的应用,我也简单的觉得使用Context就能够解决我所有的问题。说来惭愧,我基本没有思考过Redux存在的原因,可能是React真的做的太好了,又或者是我们现在的设备性能已经严重过剩了,让我完全不需要考虑应用优化的问题。

今天又冒出为什么这么多人用Redux的问题,所以又看了一下ReactRedux的文档,结果有蛮大的收获(每次看文档都有新收获,推荐大家没事多看看),突然让我回忆起曾经好多次使用useState更新数组时的别扭(虽然没什么问题,但是总觉得过于复杂了),今天我们就来聊聊这些。

随便推荐使用我写的理解例子examples/reducer-context-redux一起服用效果更佳哦,以下所有完整代码皆可在例子中找到。

需求

脱离真实需求聊一些技术的东西,总让人觉得比较虚,所以我们今天就来聊一下一个比较简单的需求,比较几种不同的方式演变的代码的区别来帮助我们理解RedcuerContextRedux这些概念。

简单的描述一下需求,一个可以创建todo的输入框,一个展示todo的列表,todo本身可以修改名称,标记为完成或者删除,具体看下图。

一般实现

从图片也可以看出来这是一个非常简单的需求,让我们来快速的实现一下,完整代码见examples/reducer-conetxt-redux/base

jsx 复制代码
...
// 定义一个 Todos
const [todos, setTodos] = useState([]);

// 定义几个方法分别用来 创建,更新,删除 Todo
const handleAddTodo = (name) => {
  setTodos([...todos, { id: nextId++, name, done: false }]);
};

const handleChangeTodo = (todo) => {
  setTodos(
    todos.map((t) => {
      if (t.id === todo.id) {
        return todo;
      } else {
        return t;
      }
    })
  );
};

const handleDelTodo = (id) => {
  setTodos(todos.filter((t) => t.id !== id));
};

为什么我只贴了一部分代码?因为这部分的代码将会进行第一步演化,我相信大部分的人应该都会这么写吧(如果不是的话,别喷我,至少我在大部分情况下都是这么写的)。

但是它有什么问题呢?其实没有什么问题,如果你还没有遇到问题的话,它的确没有问题,感觉自己在说废话呢,那我给几个你可能会遇到问题的情况吧:

  1. 如果 Todo 需要通过接口来完成创建、更新和删除,那当你同时进行多个操作时,会导致你的todos只完成了最后一次更新。
  2. 关于1有一个非常难受的地方,就是你不太能容易发现todos为什么没有正确更新成你期望的样子,这个排查是很痛苦的,不知道你们有没有遇到过?
  3. 虽然现在useState好像非常直观的展示了todos的更新机制,但如果我添加更多的功能,比如多状态的todo,这个时候你需要更多的setTodos来更新todos,这好像比较难理解,那我们可以将更新这个操作改为上一步或者下一步,这样你是不是就需要拆开handleChangeTodo这个方法了?

好了,差不多第一版就这些,那么如何来优化它?

Reducer

要用它,就得先知道它是什么吧?简单来说,就是把所有状态更新逻辑合并到一个函数中,就叫Reducer。它的定义已经出来了,我觉得这个时候你可能已经想到了如何用Reducer来更新上面的第一版了,完整代码见examples/reducer-context-redux/reducer

jsx 复制代码
function todoReducer(todos, action) {
  switch (action.type) {
    case 'added': {
      return [...todos, { id: nextId++, name: action.name, done: false }];
    }
    case 'changed':
      return todos.map((t) => {
        if (t.id === action.todo.id) {
          return action.todo;
        } else {
          return t;
        }
      });
    case 'deleted': {
      return todos.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

const [todos, dispatch] = useReducer(todoReducer, []);

const handleAddTodo = (name) => {
  dispatch({
    type: 'added',
    name,
  });
};

const handleChangeTodo = (todo) => {
  dispatch({
    type: 'changed',
    todo,
  });
};

const handleDelTodo = (id) => {
  dispatch({
    type: 'deleted',
    id,
  });
};

直观看,代码好像比上面更多了?这个例子确实是,但是就像我说的第一版中问题3那样,当你的状态越来越复杂,两种方式带来的代码增长将不会一样,也就是说,状态多到一定的程度后,这样写的代码会更少,不过这好像也不能成为一个这么写的充分理由。

那我在来补充几个这样写的好处吧:

  1. 所有的状态变更都收在了todoReducer函数里面,你可以方便的在这个函数里面console.log来感知状态的变化。 -> 方便调试
  2. todoReducer作为一个干净的函数,你可以轻易的写出它的测试用例。 -> 方便写测试用例,增强稳定性
  3. 状态的变化一目了然。 -> 增强可读性

当然这些都不是必要的,你完全可以按照你的喜好和场景来使用useState或者useReducer,不过你应该要知道它们的区别。

Context

上面的代码好像和Context并没有什么直接的联系,我又为什么要把它也拿来一起看看呢?是因为Redux就像是ReducerContext的结合体,所以你现在已经知道了Reducer是什么了,当然也要知道Context是什么,和上述一样完整代码见examples/reducer-conetxt-redux/context

那么Context又是什么呢?简单来讲,就是两个没有直接联系的组件共享状态。比如A -> B -> C -> DD想要接收到A的状态,需要经过BC,那如果你用Context就可以跳过BC,看起来是不是很好用?确实是,但是它有一个很大的缺点,也是我们不希望它被滥用的原因,因为你需要从A定义这个状态,那么如果这个状态发生了变化,A所有的子组件都会更新,那你要是在一个特别大的应用的根上定义了一个经常变化的状态,那这个应用就得经常更新,是一件比较可怕的事情。

再来看Context的主要目的是为了跨组件共享状态,它并不具有状态定义和管理的功能,也就是需要搭配useState或者useReducer来使用,这也是为什么我说Redux就像是ReducerContext的结合体,来看下代码演进吧。

jsx 复制代码
// TodoContext.jsx
import { createContext } from 'react';

export function todoReducer(todos, action) {
  switch (action.type) {
    case 'added': {
      return [...todos, { id: nextId++, name: action.name, done: false }];
    }
    case 'changed':
      return todos.map((t) => {
        if (t.id === action.todo.id) {
          return action.todo;
        } else {
          return t;
        }
      });
    case 'deleted': {
      return todos.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

export default createContext();

//
import TodoContext, { todoReducer } from './context';

const [todos, dispatch] = useReducer(todoReducer, []);

return (
  <TodoContext.Provider value={{ todos, dispatch }}>
    <ContextApp />
  </TodoContext.Provider>
);

当然这里你依然可以用useState去取代useReducer,看你如何考虑吧!

Redux

终于到了最后一步了,其实你不用Redux,好像也不会阻塞你做这个需求,或者说任何别的需求,但是为什么要用它,这就是今天要探究的内容了,一样完整代码见examples/reducer-conetxt-redux/redux

先看代码改造吧,由于新的Redux使用Toolkit来组织代码,但是为了方便理解,我还是单纯的使用redux来演示这个改造。

jsx 复制代码
// store.jsx
import { createStore } from 'redux';

function todoReducer(todos = [], action) {
  switch (action.type) {
    case 'added': {
      return [...todos, { id: nextId++, name: action.name, done: false }];
    }
    case 'changed':
      return todos.map((t) => {
        if (t.id === action.todo.id) {
          return action.todo;
        } else {
          return t;
        }
      });
    case 'deleted': {
      return todos.filter((t) => t.id !== action.id);
    }
    default:
      return todos;
  }
}

export default createStore(todoReducer);

//
import { Provider } from 'react-redux';

export default () => (
  <Provider store={store}>
    <ReduxApp />
  </Provider>
);

其实对比Context的版本来看,一眼就能明白我说的Redux就像是ReducerContext的结合体,那为什么要用它呢?直接上理由吧:

  1. 不知道你还记得Context的缺点吗?状态的变化会导致所有组件的变化,但是Redux只会影响订阅对应状态的组件。
  2. 不知道你还记得在最初版还有一个问题没有解决,就是异步请求后的状态更新,Redux有很多很好用的中间件来处理这些事情,比如redux-thunk,当然你也可以自己写,但是意义是什么呢?
  3. 浏览器插件Redux DevTools,让你清晰的看到各个状态的变化。
  4. 脱离于不同UI的状态管理,比如你同时有多个应用共享一套状态,或者说ReactVue写的两个应用共享一套状态。

这就是我理解Redux最为强大的优势吧,也就是当你没有这些问题的时候,你完全不需要它。

总结

就像我说的,当你还没有意识到你要不要用Redux时,你可能不太需要它,当你在思考如何组织你的状态,或者你已经被你的状态搞的焦头烂额了,你可能需要考虑它了,抛开需求谈技术都挺扯蛋的,你完全有足够的时间去不断的优化你的代码,而不是一开始就把所有的工具集成到一个应用里,不管它是不是真的需要,这样你永远也不会明白用它的意义是什么。

相关推荐
摸鱼的春哥8 小时前
Agent教程21:知识图谱🕸,让AI🤖学会联想
前端·javascript·后端
SuperEugene8 小时前
Vue3 组件拆分实战规范:页面 / 业务 / 基础组件边界清晰化,高内聚低耦合落地指南|Vue 组件与模板规范篇
前端·javascript·vue.js·前端框架
泯泷8 小时前
阶段二:为什么先设计指令集,编译器和运行时才能稳定对齐?
前端·javascript·架构
Dxy12393102168 小时前
HTML常用布局详解:从基础到进阶的网页结构指南
前端·html
ywf121510 小时前
前端的dist包放到后端springboot项目下一起打包
前端·spring boot·后端
恋猫de小郭10 小时前
2026,Android Compose 终于支持 Hot Reload 了,但是收费
android·前端·flutter
hpoenixf16 小时前
2026 年前端面试问什么
前端·面试
还是大剑师兰特16 小时前
Vue3 中的 defineExpose 完全指南
前端·javascript·vue.js
泯泷16 小时前
阶段一:从 0 看懂 JSVMP 架构,先在脑子里搭出一台最小 JSVM
前端·javascript·架构
mengchanmian17 小时前
前端node常用配置
前端