我有一个使用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 />
被重新渲染并将其传递给{{1} },即<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
不再依赖状态,至少对我来说是这样。< / p>
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
一起使用?
答案 0 :(得分:4)
是的。
您需要使用setTodos
const addTodo = useCallback(
(todo) => {
todo.id = id()
todo.done = false
setTodos((todos) => […todos, todo])
},
[]
)
答案 1 :(得分:1)
您已经钻了一个兔子洞!您最初的问题是您的addTodo()
函数依赖于状态todos
,因此,每更改todos
时,您都需要创建一个新的addTodo
函数并将其传递给{{1} },导致重新渲染。
您发现了NewTodo
,这可以帮助解决此问题,因为reducer传递了当前状态,因此不需要在闭包中捕获它,因此它可以在useReducer
的更改中保持稳定。但是,React作者已经考虑到了这种情况,您不需要todos
(这实际上是对喜欢Redux风格的状态更新的人们的让步!)。正如Gabriele Petrioli指出的那样,您可以只使用状态设置程序的更新用法。参见the docs。
这使您可以编写Gabriele提供的回调函数。
因此,请回答您的最终问题:
这是否意味着useCallback总是不能很好地与包含useState的函数一起播放?
useReducer
可以很好地发挥效果,但是您需要了解传递给useCallback
的闭包中捕获的内容,并且如果您使用的是useCallback
中的变量您的回调函数需要在useState
列表中传递该变量,以确保刷新您的闭包,并且不会以过期状态对其进行调用。
然后您必须意识到回调将是一个新函数,从而导致重新渲染以该回调为参数的组件。
使用useCallback来记住该功能是否被认为是从useState切换到useReducer的好处?
不,不是。 deps
不喜欢useCallback
或useState
。就像我说的,useReducer
确实是在支持另一种编程风格,不是因为它提供了其他方式无法提供的功能。
如果我不想切换到useReducer,在这种情况下是否可以将useCallback与useState一起使用?
是的,如上所述。
答案 2 :(得分:1)
如Gabriele Petrioli所述,您可以使用回调语法,也可以将回调的依赖项值保留在ref中,并在您的回调中使用该ref而不是状态as mentioned here。 / p>
在您的示例中,这种方法如下所示:
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]
)