这是一次突发奇想的感悟,感觉还挺神奇的,遂记录一下。
前言
作为一个React
的开发者已经蛮久的了,大大小小的应用也开发了不少,除了一开始学习React
时用过Redux
以外,后来基本都不碰了,不管多么复杂的应用,我也简单的觉得使用Context
就能够解决我所有的问题。说来惭愧,我基本没有思考过Redux
存在的原因,可能是React
真的做的太好了,又或者是我们现在的设备性能已经严重过剩了,让我完全不需要考虑应用优化的问题。
今天又冒出为什么这么多人用Redux
的问题,所以又看了一下React
和Redux
的文档,结果有蛮大的收获(每次看文档都有新收获,推荐大家没事多看看),突然让我回忆起曾经好多次使用useState
更新数组时的别扭(虽然没什么问题,但是总觉得过于复杂了),今天我们就来聊聊这些。
随便推荐使用我写的理解例子examples/reducer-context-redux一起服用效果更佳哦,以下所有完整代码皆可在例子中找到。
需求
脱离真实需求聊一些技术的东西,总让人觉得比较虚,所以我们今天就来聊一下一个比较简单的需求,比较几种不同的方式演变的代码的区别来帮助我们理解Redcuer
、Context
和Redux
这些概念。
简单的描述一下需求,一个可以创建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));
};
为什么我只贴了一部分代码?因为这部分的代码将会进行第一步演化,我相信大部分的人应该都会这么写吧(如果不是的话,别喷我,至少我在大部分情况下都是这么写的)。
但是它有什么问题呢?其实没有什么问题,如果你还没有遇到问题的话,它的确没有问题,感觉自己在说废话呢,那我给几个你可能会遇到问题的情况吧:
- 如果 Todo 需要通过接口来完成创建、更新和删除,那当你同时进行多个操作时,会导致你的
todos
只完成了最后一次更新。 - 关于
1
有一个非常难受的地方,就是你不太能容易发现todos
为什么没有正确更新成你期望的样子,这个排查是很痛苦的,不知道你们有没有遇到过? - 虽然现在
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
那样,当你的状态越来越复杂,两种方式带来的代码增长将不会一样,也就是说,状态多到一定的程度后,这样写的代码会更少,不过这好像也不能成为一个这么写的充分理由。
那我在来补充几个这样写的好处吧:
- 所有的状态变更都收在了
todoReducer
函数里面,你可以方便的在这个函数里面console.log
来感知状态的变化。 -> 方便调试 todoReducer
作为一个干净的函数,你可以轻易的写出它的测试用例。 -> 方便写测试用例,增强稳定性- 状态的变化一目了然。 -> 增强可读性
当然这些都不是必要的,你完全可以按照你的喜好和场景来使用useState
或者useReducer
,不过你应该要知道它们的区别。
Context
上面的代码好像和Context
并没有什么直接的联系,我又为什么要把它也拿来一起看看呢?是因为Redux
就像是Reducer
和Context
的结合体,所以你现在已经知道了Reducer
是什么了,当然也要知道Context
是什么,和上述一样完整代码见examples/reducer-conetxt-redux/context 。
那么Context
又是什么呢?简单来讲,就是两个没有直接联系的组件共享状态。比如A -> B -> C -> D
,D
想要接收到A
的状态,需要经过B
和C
,那如果你用Context
就可以跳过B
和C
,看起来是不是很好用?确实是,但是它有一个很大的缺点,也是我们不希望它被滥用的原因,因为你需要从A
定义这个状态,那么如果这个状态发生了变化,A
所有的子组件都会更新,那你要是在一个特别大的应用的根上定义了一个经常变化的状态,那这个应用就得经常更新,是一件比较可怕的事情。
再来看Context
的主要目的是为了跨组件共享状态,它并不具有状态定义和管理的功能,也就是需要搭配useState
或者useReducer
来使用,这也是为什么我说Redux
就像是Reducer
和Context
的结合体,来看下代码演进吧。
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
就像是Reducer
和Context
的结合体,那为什么要用它呢?直接上理由吧:
- 不知道你还记得
Context
的缺点吗?状态的变化会导致所有组件的变化,但是Redux
只会影响订阅对应状态的组件。 - 不知道你还记得在最初版还有一个问题没有解决,就是异步请求后的状态更新,
Redux
有很多很好用的中间件来处理这些事情,比如redux-thunk
,当然你也可以自己写,但是意义是什么呢? - 浏览器插件
Redux DevTools
,让你清晰的看到各个状态的变化。 - 脱离于不同
UI
的状态管理,比如你同时有多个应用共享一套状态,或者说React
和Vue
写的两个应用共享一套状态。
这就是我理解Redux
最为强大的优势吧,也就是当你没有这些问题的时候,你完全不需要它。
总结
就像我说的,当你还没有意识到你要不要用Redux
时,你可能不太需要它,当你在思考如何组织你的状态,或者你已经被你的状态搞的焦头烂额了,你可能需要考虑它了,抛开需求谈技术都挺扯蛋的,你完全有足够的时间去不断的优化你的代码,而不是一开始就把所有的工具集成到一个应用里,不管它是不是真的需要,这样你永远也不会明白用它的意义是什么。