React 状态管理 - useState 和 useReducer 如何选择?

React 是一个用于构建用户界面的 JavaScript 库,解决了视图 UI 层问题。但是对于状态管理却没有给出一个好的解决方案,状态大致分为:反映 UI 变化的状态、本地创建的状态、服务器返回的数据状态等,面对社区众多的状态管理库,做为初学者不知道如何更好的选择。.

本系列文章 “React 状态管理” 也是希望能帮助到大家了解不同状态管理方案(React 官方本身提供的及社区的一些库)能做的事情有哪些及优略点。

本篇先从 React 本身提供的组件状态管理开始。React 的组件状态分为类组件时代的 this.state 和当下函数组件的 useState/useReducer() hooks API 管理数据状态。

类组件 State

类组件将组件内所有的状态放在一个 state 中管理:this.state 获取状态数据、this.setState() 更新状态数据。

import React from 'react';

class State extends React.Component {
  constructor() {
    super();
    this.state = {
      count: 0,
    }
  }

  render() {
    return <div>
      <p>count: {this.state.count}</p>
      <button onClick={() => 
        this.setState({ count: this.state.count + 1 })
      }> 类组件计数 </button>
    </div>
  }
}
export default State;

函数组件 useState

React 16.8 发布后,函数组件成为热门选择,组件状态使用 useState() 函数,相比类组件状态拆分的更细并且每次更新状态变量都是替换而不是合并

如果更新依赖于先前的状态,可以将函数传递给 setState,并返回一个更新后的值 setState(preState => newState) 熟悉 Redux 的朋友看起来也许不陌生,该函数是不是更像一个 reducer 函数?

Example:useState 实现计数器增、减、重置。

import { useState } from 'react';
const State = () => {
  const [count, setCount] = useState(0);

  return <div>
    <p>计数器 count: {count}</p>
    <button onClick={() => setCount(count + 1)}> 增 </button>
    <button onClick={() => setCount(preCount => preCount - 1)}> 减 </button>
    <button onClick={() => setCount(0)}> 重置 </button>
  </div>
}
export default State;

函数组件 useReducer

刚开始看到 useReducer 时,还联想和 Redux 之间有什么关系?需要注意的是和 Redux 完全不是一个东西,useReducer 是 React Hooks 提供的一个 API,如果熟悉 Redux 可能看起来会更熟悉

useReducer 是 useState 的替代方案,主要用来处理一些逻辑比较复杂的 state 或 下一个 state 依赖于前一个 state 等。

Example:使用 useReducer 重写上面的计数器示例,可以和上面的 useState 做一个对比。

import { useReducer } from "react"

const COUNT_INCREMENT = 'COUNT_INCREMENT';
const COUNT_DECREMENT = 'COUNT_DECREMENT';
const COUNT_RESET = 'COUNT_RESET';

const initialState = {
  count: 0,
};

const reducer = (state, action) => {
  switch (action.type) {
    case COUNT_INCREMENT:
      return { count: state.count + 1 };
    case COUNT_DECREMENT:
      return { count: state.count - 1 };
    case COUNT_RESET:
      return { count: initialState.count };
    default:
      return state;
  }
};

const State = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return <div>
    <p>计数器 count: {state.count}</p>
    <button onClick={() => dispatch({ type: COUNT_INCREMENT })}> 增 </button>
    <button onClick={() => dispatch({ type: COUNT_DECREMENT })}> 减 </button>
    <button onClick={() => dispatch({ type: COUNT_RESET })}> 重置 </button>
  </div>
}

export default State;

useReducer 实现 Todos

使用 useReducer 实现一个稍微复杂点的功能,例如 Todos,有创建、更新、移除待办事项功能,你可以试想下如果换做 useState 该怎么处理?会不会更麻烦?

reducer 实现

在 src/reducers 目录下实现 reducer 需要的逻辑,定义的 initialState 变量、reducer 函数都是为 useReducer 这个 Hook 做准备的,这里的 reducer 函数是一个纯函数。

// src/reducers/todos-reducer.jsx
export const TODO_LIST_ADD = 'TODO_LIST_ADD';
export const TODO_LIST_EDIT = 'TODO_LIST_EDIT';
export const TODO_LIST_REMOVE = 'TODO_LIST_REMOVE';

const randomID = () => Math.floor(Math.random() * 10000);
export const initialState = {
  todos: [{ id: randomID(), content: 'todo list' }],
};

const reducer = (state, action) => {
  switch (action.type) {
    case TODO_LIST_ADD: {
      const newTodo = {
        id: randomID(),
        content: action.payload.content
      };
      return {
        todos: [ ...state.todos, newTodo ],
      }
    }
    case TODO_LIST_EDIT: {
      return {
        todos: state.todos.map(item => {
          const newTodo = { ...item };
          if (item.id === action.payload.id) {
            newTodo.content = action.payload.content;
          }
          return newTodo;
        })
      }
    }
    case TODO_LIST_REMOVE: {
      return {
        todos: state.todos.filter(item => item.id !== action.payload.id),
      }
    }
    default: return state;
  }
}

export default reducer;

创建 Todos 组件

下面进行待办事项组件开发,细分为 TodoAdd、Todo、Todos 三个组件,在 Todos 组件内使用 useReducer 获取组件需要的状态(state)和更新状态的方法(dispatch)。

添加、更新、删除待办事项行为分别在 TodoAdd、Todo 组件中,以下我们使用的是 React 推荐的 组合组件模式,可以通过 props 从父组件传递数据到子组件

import { useReducer, useState } from "react";
import reducer, { initialState, TODO_LIST_ADD, TODO_LIST_EDIT, TODO_LIST_REMOVE } from "../../reducers/todos-reducer";

const TodoAdd = ({ dispatch }) => {
  const [content, setContent] = useState('');

  return <>
    <input type="text" onChange={e => setContent(e.target.value)} />
    <button onClick={() => {
      dispatch({ type: TODO_LIST_ADD, payload: { content } })
    }}>
      添加
    </button>
  </>
};

const Todo = ({ todo, dispatch }) => {
  const [isEdit, setIsEdit] = useState(false);
  const [content, setContent] = useState(todo.content);

  return <div>
    {
      !isEdit ? <>
        <span>{todo.content}</span>
        <button onClick={() => setIsEdit(true)}> 编辑 </button>
        <button onClick={() => dispatch({ type: TODO_LIST_REMOVE, payload: { id: todo.id } })}> 删除 </button>
      </> : <>
        <input value={content} type="text" onChange={ e => setContent(e.target.value) } />
        <button onClick={() => {
          setIsEdit(false);
          dispatch({ type: TODO_LIST_EDIT, payload: { id: todo.id, content } })
        }}> 更新 </button>
        <button onClick={() => setIsEdit(false)}> 取消 </button>
      </>
    }
  </div>
}

const Todos = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return <>
    <TodoAdd dispatch={dispatch} />
    {
      state.todos.map(todo => <Todo
        key={todo.id}
        todo={todo}
        dispatch={dispatch}
      />)
    }
  </>
}

export default Todos;

总结

useState 适合定义一些单一的状态,useReducer 适合逻辑比较复杂的 state。

一个好的经验法则是:“如果状态是基本的数据类型(字符串、数值型、布尔型)可以选择 useState,如果是数组或对象等其它复杂的类型或复杂逻辑时最好选择 useReducer”。

但是两者都是组件的状态管理,当多个组件协同工作时不得不面对组件间 “数据通信” 问题

React 组件数据状态流转是一个 “单项数据流” 的理念。简单的组件可以通过 props 传递数据状态,当组件过多时,层层 Props 传递的方式显的就有点过于繁琐了,不易维护。