我有一个非常简单的待办事项应用程序,使用 React 构建。
App.js
看起来像这样
const App = () => {
const [todos, setTodos] = useState(initialState)
const addTodo = (todo) => {
todo.id = id()
todo.done = false
setTodos([...todos, todo])
}
const toggleDone = (id) => {
setTodos(
todos.map((todo) => {
if (todo.id !== id) return todo
return { ...todo, done: !todo.done }
})
)
}
return (
<div className="App">
<NewTodo onSubmit={addTodo} />
<Todos todos={todos} onStatusChange={toggleDone} />
</div>
)
}
export default App
其中<NewTodo>
是呈现输入表单以提交新待办事项的组件,<Todos />
是呈现待办事项列表的组件。
现在的问题是,当我切换/更改现有的待办事项时,<NewTodo>
将被重新渲染,因为<App />
被重新渲染并且它传递给<NewTodo>
的道具也会addTodo
改变。由于它是每次渲染的新<App />
其中定义的函数也将是一个新函数。
为了解决这个问题,我首先将<NewTodo>
包装在React.memo
中,这样当道具没有改变时,它会跳过重新渲染。我想使用useCallback
来获得记忆addTodo
,这样<NewTodo>
就不会得到不必要的重新渲染。
const addTodo = useCallback(
(todo) => {
todo.id = id()
todo.done = false
setTodos([…todos, todo])
},
[todos]
)
但我意识到,显然addTodo
取决于todos
哪个状态是保存现有待办事项的状态,并且当您切换/更改现有待办事项时,它正在发生变化。所以这个记忆功能也会改变。
然后我把我的应用程序从使用useState
切换到useReducer
,我发现突然间我的addTodo
不依赖于状态,至少在我看来是这样。
const reducer = (state = [], action) => {
if (action.type === TODO_ADD) {
return [...state, action.payload]
}
if (action.type === TODO_COMPLETE) {
return state.map((todo) => {
if (todo.id !== action.payload.id) return todo
return { ...todo, done: !todo.done }
})
}
return state
}
const App = () => {
const [todos, dispatch] = useReducer(reducer, initialState)
const addTodo = useCallback(
(todo) => {
dispatch({
type: TODO_ADD,
payload: {
id: id(),
done: false,
...todo,
},
})
},
[dispatch]
)
const toggleDone = (id) => {
dispatch({
type: TODO_COMPLETE,
payload: {
id,
},
})
}
return (
<div className="App">
<NewTodo onSubmit={addTodo} />
<Todos todos={todos} onStatusChange={toggleDone} />
</div>
)
}
export default App
正如你在这里看到的addTodo
只是宣布发生在状态上的操作,而不是做一些与状态直接相关的事情。所以这会起作用
const addTodo = useCallback(
(todo) => {
dispatch({
type: TODO_ADD,
payload: {
id: id(),
done: false,
...todo,
},
})
},
[dispatch]
)
我的问题是,这是否意味着useCallback
永远不会很好地与包含useState
的函数一起使用?这种使用useCallback
记忆功能的能力被认为是从useState
切换到useReducer
的好处吗?如果我不想切换到useReducer
,在这种情况下,有没有办法将useCallback
与useState
一起使用?
是的,有。
您需要使用setTodos
的更新函数语法
const addTodo = useCallback(
(todo) => {
todo.id = id()
todo.done = false
setTodos((todos) => […todos, todo])
},
[]
)
你已经潜入了一个兔子洞!你最初的问题是你的addTodo()
函数依赖于状态todos
,因此每当todos
发生变化时,你需要创建一个新的addTodo
函数并将其传递给NewTodo
,导致重新渲染。
您发现了useReducer
,这可能有助于解决这个问题,因为化简器通过当前状态,因此不需要在闭包中捕获它,因此它可以在todos
的变化中保持稳定。但是,React 的作者已经想到了这种情况,你不需要useReducer
(这实际上是作为对那些喜欢 Redux 风格的状态更新的人的让步!正如 Gabriele Petrioli 指出的那样,您可以使用状态设置器的更新用法。请参阅文档。
这允许您编写 Gabriele 提供的回调函数。
所以要回答你的最后一个问题:
这是否意味着 useCallback 总是不能很好地与包含 useState 的函数配合使用?
useCallback
可以玩得非常好,但你需要知道你在传递给useCallback
的闭包中捕获了什么,如果你在回调中使用来自useState
的变量,你需要在deps
列表中传递该变量,以确保你的闭包被刷新并且不会被调用过时状态。
然后你必须意识到回调将是一个新函数,从而导致重新呈现到将其作为参数的组件。
这种使用 useCallback 来记忆函数的能力是否被认为是从 useState 切换到 useReducer 的好处?
不,不是真的。useCallback
不喜欢useState
或useReducer
.正如我所说,useReducer
确实是为了支持不同的编程风格,而不是因为它提供了通过其他方式无法获得的功能。
如果我不想切换到useReducer,在这种情况下有没有办法将useCallback与useState一起使用?
是的,如上所述。
正如 Gabriele Petrioli 所提到的,您可以使用回调语法,也可以将回调依赖项的值保留在 ref 中,并在回调中使用该 ref 而不是此处提到的状态。
在您的示例中,此方法如下所示:
const [todos, setTodos] = useState([]);
const todosRef = useRef(todos);
useEffect(() => {
todosRef.current = todos;
},[todos]);
const addTodo = useCallback(
todo => {
todo.id = id()
todo.done = false
setTodos([…todosRef.current, todo])
},
[todosRef]
)