React useReducer hook

377 阅读4分钟

ref

管理组件状态的 useState hook

这是最简单的hook,使用示例如下:

import React, { useState } from "react";

const Counter = () => {
  const [clicks, setClicks] = useState(0);
  const increment = () => setClicks(clicks + 1);

  return (
    <>
      <p>{clicks} clicks</p>
      <button onClick={increment}>Click</button>
    </>
  );
}

useState hook返回的更新函数工作方式与之前React setState很类似,除了新值用传入的参数完全替换state而非merge旧state。由于我们可以想用多少用多少useState函数,我们可以不管其形状地创建并操作组件state。

import React, { useState } from "react";

const Counter = () => {
  const [clicks, setClicks] = useState(0);
  const [disabled, setDisableStatus] = useState(false);
  
  const increment = () => setClicks(clicks + 1);
  const toggleDisable = () => setDisableStatus(!disabled);

  return (
    <>
      <p>{clicks} clicks</p>
      <button onClick={increment} disabled={disabled}>Click</button>
      <button onClick={toggleDisable}>{disabled ? "enable" : "disable"}</button>
    </>
  );
}

然而,如果我们的组件需要保持一些逻辑类似的各部分数据,useState可能变得些许笨重。比如下边一个可以加减重置其值且可以undo上次操作的counter。使用undo操作在每次操作时,在当前state之外,我们还需要记录上次state:

import React, { useState } from "react";

const Counter = ({ initialValue }) => {
  
  const [prevValue, setPrevValue] = useState(null);
  const [clicks, setClicks] = useState(initialValue);
  
  const increment = () => {
    setPrevValue(clicks);
    setClicks(clicks + 1);
  };
  
  const decrement = () => {
    setPrevValue(clicks);
    setClicks(clicks - 1);
  };
  
  const reset = () => {
    setPrevValue(null);
    setClicks(initialValue);
  };
  
  const undo = () => {
    setClicks(prevValue);
    setPrevValue(null);
  };

  return (
    <>
      <p>{clicks} clicks</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
      <button onClick={reset}>Reset</button>
      <button onClick={undo} disabled={!prevValue}>Undo</button>
    </>
  );
}

useReducer to the rescue!

useReducer 是管理组件state的另一个hook。看起来如下 :

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

The reducer必须是接收一个state和一个action并返回一个新state的函数。如下是利用useReducer重写上个示例的例子。

import React, { useReducer } from "react";

const reducer = (state, action) => {
  switch (action.type) {
    case "reset":
      return {
        clicks: initialValue,
        prevValue: null,
      };
    case "increment":
      return {
        clicks: state.clicks + 1,
        prevValue: state.clicks,
      };
    case "decrement":
      return {
        clicks: state.clicks - 1,
        prevValue: state.clicks,
      };
    case "undo":
      return {
        clicks: state.prevValue,
        prevValue: null,
      };
    default:
      return state;
  }
};  

const Counter = ({ initialValue }) => {
  
  const [state, dispatch] = useReducer(reducer, {clicks: initialValue, prevValue: null});

  return (
    <>
      <p>{clicks} clicks</p>
      <button onClick={() => dispatch({type: "increment"})}>Increment</button>
      <button onClick={() => dispatch({type: "decrement"})}>Decrement</button>
      <button onClick={() => dispatch({type: "reset"})}>Reset</button>
      <button onClick={() => dispatch({type: "undo"})} disabled={!state.prevValue}>
        Undo
      </button>
    </>
  );
}

此时,在当一个action一旦被trigger时所有state改变之外,我们可以找到同一个state相关的所有逻辑。Dan Abramov 在twitter里总结了useState与useReducer的使用场合

Avoid passing callbacks down

利用useReducer我们可以简化大组件树传递callback问题。

import React, { useState } from "react";

const AddTodoBtn = ({ handleAddTodo }) => (
  <div className="action-add">
    <button onClick={handleAddTodo}>Add new todo</button>
  </div>
);

const RemoveAllBtn = ({ handleRemoveAll }) => (
  <div className="action-remove-all">
    <button onClick={handleRemoveAll}>Remove all todos</button>
  </div>
);

// This component and every one placed between the TodoList
// and the final component which will use a TodoList callback
// should pass it down.
const Actions = ({ addTodo, removeAll, ...rest }) => (
  <div className="actions-container">
    <AddTodoBtn handleAddTodo={addTodo} />
    <RemoveAllBtn handleRemoveAll={removeAll} />
    //...more actions
  </div>
);

const TodoList = () => {
  const [todos, setTodos] = useState([]);
  const addTodo = () => setTodos([...todos, {}]);
  const removeAll = () => setTodos([]);
  
  return (
    <div className="todo-list">
      <Actions addTodo={addTodo} removeAll={removeAll}/>
      //...todos
    </div>
  );
};

我们可以简化如上例子,通过定义一个包含所有callback以便向下传递的object。为避免传递地狱,我们可以通过context传递‘api object’。问题是object在每次render中改变,故所有读取其值的组件也将rerender。Abramov 推荐一种类似模式,不是自上而下传递api object,而是传递dispatch function。该模式的关键在于dispatch函数在每次render时不会改变:

import React, { useReducer, createContext, useContext } from "react";

const AddTodoBtn = () => {
  const dispatch = useContext(TodosDispatch);
  
  return (
    <div className="action-add">
      <button onClick={() => dispatch({ type: "add" })}>Add new todo</button>
    </div>
  );
};

const RemoveAllBtn = () => {
  const dispatch = useContext(TodosDispatch);
  
  return (
    <div className="action-remove-all">
      <button onClick={() => dispatch({ type: "removeAll" })}>Remove all todos</button>
    </div>
  );
};

const Actions = () => (
  <div className="actions-container">
    <AddTodoBtn />
    <RemoveAllBtn />
    //...more actions
  </div>
);

const TodosDispatch = createContext(null);

const reducer = (state, action) => {
  switch (action.type) {
    case "add":
      return { todos: [...state.todos, {}] };
    case "removeAll":
      return { todos: [] };
    default:
      return state;
  }
}; 

const TodoList = () => {
    
  const [state, dispatch] = useReducer(reducer);
  
  return (
    <div className="todo-list">
      <TodosDispatch.Provider value={dispatch}>
        <Actions />
        //...state.todos
      </TodosDispatch.Provider>
    </div>
  );
};

Wrapping up

你可以用useState去管理任何组件的state,但有两种场景useReducer更胜一筹:

  • When the state keeps data which change together (like “data” and “isLoadingData”).
  • When you have to pass callbacks down in large component trees.