管理组件状态的 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.