从零开始学习React-5:状态与状态管理


theme: devui-blue

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情

通过前面的几篇文章,我们已经大致的了解了 React 中的一些概念,也通过 cra 脚手架创建了一个正式的项目,接下来我们将使用 Antd 组件库开发一些简单的用例。并通过这个例子进一步了解 React 函数式组件、HOOK 、状态管理等概念。

在这个系列文章的第一章,我们就介绍过了 React 的状态,以及一种状态管理思想:状态提升,但是在面对一些复杂场景,状态提升就显得有些麻烦了,多个不同层级的组件状态,提升到共有的顶层组件,然后通过 props 在组件之间传递,一来代码量上提升很多,二来如果涉及修改,就会比较麻烦。

有的中间组件可能并不需要使用这些状态,或者函数,但是为了传递到目标组件还是需要在 props 中进行声明,显得非常的笨重。我写了一个 TodoList 的小例子,来说明这种尴尬的情况。

通过状态提升管理状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
const App = () => {
// 在顶层组件声明状态与改变状态的函数(称之为事件)
const [todos, setTodos] = React.useState([])
const addTodo = (todo) => {
setTodos([...todos, todo])
}
const delTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id))
}
return (
<div>
<Header addTodo={addTodo} />
<List todos={todos} delTodo={delTodo} />
</div>
);
};

// 处理输入的组件
const Header = ({ addTodo }) => {
//处理键盘事件
const handleKeyUp = (event) => {
const { keyCode, target } = event
if (keyCode !== 13) return
if (target.value.trim() === '') {
alert('不可以输入空')
return
} else {
const todoObj = { id: nanoid(), name: target.value }
addTodo(todoObj)
target.value = ''
}
}
return (
<div>
<input
style={{ width: '400px' }}
onKeyUp={handleKeyUp}
type="text"
placeholder='请输入你的任务名称,按回车键确认'
/>
</div>
)
}

// 用于显示列表的组件
const List = ({ todos, delTodo }) => {
return (
<ul>
{todos.map(todo => (
<Item key={todo.id} item={todo} delTodo={delTodo} />
))}
</ul>
)
}

// 展示具体todo的组件
const Item = ({ item, delTodo }) => {
return (
<li >
<span>{item.name}</span>
<button onClick={() => delTodo(item.id)}>删除</button>
</li>
)
}

可以看出顶层组件 App 不得已承担了状态管理的工作,由他持有状态,分发状态、改变状态的函数给其子组件。

其中 List 组件,虽然他本身并不使用 delTodo ,但是因为 状态提升 的原因,Item 组件想要拿到delTodo函数,必须由他中转。

这种组织、管理状态的思想称之为:单向数据流,即状态(数据)从父组件向下流向子组件,数据只有一个唯一可信源,就是来自父组件的状态。子组件从过向上传递事件(通过调用由父组件传递的改变状态的函数实现传递),来改变状态。

可以预想,当一个页面存在诸多复杂状态、组件嵌套时,使用状态提升反而会让代码难以管理,难以预测。

这时候就不得不请出 React 中最流行的状态管理库:Redux

使用Redux管理状态

0.安装依赖与原理图

执行:yarn add redux react-redux 安装 redux 与 react-redux。

redux原理图

redux 的理念其实很简单,由如下三个关键点:

  1. reducer 状态处理函数
  2. store 状态中心
  3. action 动作

一句话总结其工作原理就是:通过在组件中调用dispatch 函数传递一个 action 对象给storestore通过传入旧的状态actionreducer函数,获取到新的状态。

1. 类式组件中的使用

1、创建对应状态的 reducer文件,这里我们称之为 todo.reducer.jsreducer 在redux中是一个特殊的纯函数,可以理解为state的加工工厂,专门用于处理state。

reducer函数要求接受两个参数,分别是 preState 与 action,一个标准写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
const initState = [];

export default function todoReducer(preState = initState, action) {
const { type, data } = action;
switch (type) {
case "addTodo":
return [...preState, data];
case "delTodo":
return preState.filter(item => item.id !== data);
default:
return preState;
}
}

第一个参数是前一个状态,一般需要给一个初始值,建议在函数外定义,第二个参数是action,即动作;

一般的每个 action 动作都有一个typedata,通过 type 判断来如何操作 state,函数的返回值就是加工完毕的 newState

2、需要创建store.js文件,store是一个实例对象是一个桥梁,

他暴露dispatch函数给组件使用,自己接受reducer函数,使用redux的标准函数createStore可以创建

1
2
3
4
5
6
import { legacy_createStore as createStore } from "redux";
// 导入默认暴露的 reducer 函数
import todoReducer from "./todo.reducer";

export default createStore(todoReducer);

3、订阅store,组件需要订阅 store,才能知道 store 中保存的 reducer 发生调用,

否则只是 dispatch(action),只会改变store中保存的状态,一般在组件挂载时订阅

1
2
3
4
5
componentDIDMount(){
store.subscribe(()=>{
this.setState({})
})
}

在不使用react-redux的情况下,store中状态更新时是不会通知组件变化的,因此需要手动订阅,然后调用 this.setState({}),手动使类式组件刷新;

4、分配动作,调用store的dispatch函数,传递 action 即可

2. 在函数式组件中使用react-redux的hooks

首先需要安装 react-redux :yarn add react-reudx

react-redux提供了两个关键的hooks函数:

  1. useSelector
  2. useDispatch

引入:import { useDispatch, useSelector } from'react-redux'

使用的方法与在类式组件中使用redux的前两部是类似的,但是在订阅与发布处是不同的;

1、暴露store对象

我们一般使用 React-redux 提供的<Provider> 组件包裹 App 组件,通过 store={store} 来全局暴露store对象

2、使用 useSelector 钩子函数来获取我们关注的状态

1
const todos = useSelector(state => state)

该函数接受的函数参数为reducer中指定的state,通过返回值暴露state中具体需要关注的属性。

此处因为我们只有一个 reducer 、也只有一个状态,所以返回的就是 state 本身,如果有多个 reducer 需要使用 combineReducers 函数来合并多个reducer ,代码如下:

1
2
3
4
5
6
// 传给combineReducers函数的是一个对象我们在这里可以为各个 reducer 函数命名
const rootReducer = combineReducers({
todos: todoReducer,
persons: personReducer,
});
export default createStore(rootReducer);

当有多个reducer、一个reducer管理多个状态时,选择器的使用方式是这样:

state.<命名的reducer函数>.<reducer的状态中的某个状态>

例如 const selector = state => state.todos.xxxx

3、使用 useDispatch 钩子获得到 dispatch 函数对象

4、调用 dispatch() 传递 action 给 reducer,当reducer变更状态后无需手动刷新页面,

此时从 useSelector 中获取的状态对象就如真正的状态是一样的,可以自行更新渲染组件

3. 实操:使用redux改写todo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import React from "react";
import { useDispatch, useSelector, Provider } from 'react-redux';
import { nanoid } from 'nanoid'
import store from './store'

const App = () => {
// 在顶层暴露 store 对象
return (
<Provider store={store}>
<div>
<Header />
<List />
</div>
</Provider>
);
};

// 处理输入的组件
const Header = () => {
const dispatch = useDispatch();
//处理键盘事件
const handleKeyUp = (event) => {
const { keyCode, target } = event
if (keyCode !== 13) return
if (target.value.trim() === '') {
alert('不可以输入空')
return
} else {
const data = { id: nanoid(), name: target.value }
// 通过 dispatch 分配动作
dispatch({ type: 'addTodo', data })
target.value = ''
}
}
return (
<div>
<input style={{ width: '400px' }} onKeyUp={handleKeyUp} type="text" placeholder='请输入你的任务名称,按回车键确认' />
</div>
)
}

// 用于显示列表的组件
const List = () => {
// 通过useSelector选择器拿到由 redux 帮我们管理的对象
const todos = useSelector(state => state)
return (
<ul>
{todos.map(todo => (
<Item key={todo.id} item={todo} />
))}
</ul>
)
}

// 展示具体todo的组件
const Item = ({ item }) => {
const dispatch = useDispatch();
const handleDeleteTodo = () => {
dispatch({ type: 'delTodo', data: item.id })
}
return (
<li >
<span>{item.name}</span>
<button onClick={handleDeleteTodo}>删除</button>
</li>
)
}

export default App;

总结

上面的代码中的关键部分我都有注释,相比通过这个例子的对比,大家可以很好的理解状态管理的概念,与如何使用状态提升、redux 进行状态管理!