[英]use `useCallback` only work with `useReducer`?
I have a very simple todo app built with React.我有一个用 React 构建的非常简单的待办事项应用程序。
The App.js
looks like this 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
where <NewTodo>
is the component that renders the input form to submit new todo item and <Todos />
is the component that renders the list of the todo items.其中
<NewTodo>
是呈现输入表单以提交新待办事项的组件, <Todos />
是呈现待办事项列表的组件。
Now the problem is that when I toggle/change an existing todo item, the <NewTodo>
will get re-rendered since the <App />
gets re-rendered and the prop it passes to <NewTodo>
, which is addTodo
will also change.现在的问题是,当我切换/更改现有的待办事项时,
<NewTodo>
将被重新渲染,因为<App />
被重新渲染并且它传递给<NewTodo>
的道具,即addTodo
也会改变. Since it is a new <App />
every render the function defined in it will also be a new function.因为它是一个新的
<App />
每次渲染其中定义的 function 也将是一个新的 function。
To fix the problem, I first wrapped <NewTodo>
in React.memo
so it will skip re-renders when the props didn't change.为了解决这个问题,我首先将
<NewTodo>
包装在React.memo
中,这样当道具没有改变时它会跳过重新渲染。 And I wanted to use useCallback
to get a memoized addTodo
so that <NewTodo>
will not get unnecessary re-renders.而且我想使用
useCallback
来获得一个记忆化的addTodo
,这样<NewTodo>
就不会得到不必要的重新渲染。
const addTodo = useCallback(
(todo) => {
todo.id = id()
todo.done = false
setTodos([…todos, todo])
},
[todos]
)
But I realized that obviously addTodo
is dependent upon todos
which is the state that holds the existing todo items and it is changing when you toggle/change an existing todo item.但我意识到
addTodo
显然依赖于todos
,它是 state ,它包含现有的待办事项,并且当您切换/更改现有的待办事项时它会发生变化。 So this memoized function will also change.所以这个 memoized function 也会改变。
Then I switched my app from using useState
to useReducer
, I found that suddenly my addTodo
is not dependent upon the state, at least that's what it looks like to me.然后我将我的应用程序从使用
useState
切换到useReducer
,我发现我的addTodo
突然不依赖于 state,至少在我看来是这样。
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
As you can see here addTodo
is only announcing the action that happens to the state as opposed to doing something directly related to the state. So this would work正如您在此处看到的,
addTodo
仅宣布发生在 state 上的操作,而不是执行与 state 直接相关的操作。所以这会起作用
const addTodo = useCallback(
(todo) => {
dispatch({
type: TODO_ADD,
payload: {
id: id(),
done: false,
...todo,
},
})
},
[dispatch]
)
My question is, does this mean that useCallback
never plays nicely with functions that contain useState
?我的问题是,这是否意味着
useCallback
永远不能很好地与包含useState
的函数一起使用? Is this ability to use useCallback
to memoize the function considered a benefit of switching from useState
to useReducer
?这种使用
useCallback
来记忆 function 的能力是否被认为是从useState
切换到useReducer
的好处? If I don't want to switch to useReducer
, is there a way to use useCallback
with useState
in this case?如果我不想切换到
useReducer
,在这种情况下有没有办法将useCallback
与useState
一起使用?
Yes there is.就在这里。
You need to use the update function syntax of the setTodos
您需要使用 setTodos 的更新
setTodos
语法
const addTodo = useCallback(
(todo) => {
todo.id = id()
todo.done = false
setTodos((todos) => […todos, todo])
},
[]
)
You've dived down a bit of a rabbit hole!你掉进了一个兔子洞! Your original problem was that your
addTodo()
function depended on the state todos
, therefore whenever todos
changed you needed to create a new addTodo
function and pass that to NewTodo
, causing a re-render.您最初的问题是您的
addTodo()
function 依赖于 state todos
,因此每当todos
更改时,您需要创建一个新的addTodo
function 并将其传递给NewTodo
,从而导致重新渲染。
You discovered useReducer
, which could help with this since the reducer is passed the current state, and so does not need to capture it in the closure, so it can be stable over changes of todos
.你发现了
useReducer
,它可以帮助解决这个问题,因为 reducer 传递了当前的 state,所以不需要在闭包中捕获它,所以它可以在todos
的变化中保持稳定。 However, the React authors have already thought of this situation, and you don't need useReducer
(which is really provided as a concession to those who like the Redux style of state updating.), As Gabriele Petrioli pointed out.然而,React 的作者已经想到了这种情况,你不需要
useReducer
(这实际上是对那些喜欢 Redux 风格的 state 更新的人的让步。),正如 Gabriele Petrioli 指出的那样。 you can just use the update usage of the state setter.您可以只使用 state 设置器的更新用法。 See the docs .
请参阅文档。
This allows you to write the callback function that Gabriele has provided.这允许您编写 Gabriele 提供的回调 function。
So to answer your final questions:所以回答你最后的问题:
does this mean that useCallback always does not play nicely with function that contains useState?
这是否意味着 useCallback 始终不能很好地与包含 useState 的 function 一起播放?
useCallback
can play perfectly nicely, but you need to be aware of what you are capturing in the closure you pass to useCallback
, and if you are using a variable from useState
in your callback you need to pass that variable in the list of deps
to ensure that your closure is refreshed and it won't be called with out-of-date state. useCallback
可以很好地播放,但是你需要知道你在传递给useCallback
的闭包中捕获了什么,如果你在回调中使用来自useState
的变量,你需要在deps
列表中传递该变量以确保您的关闭已刷新,并且不会使用过时的 state 调用它。
And then you have to realize that the callback will be a new function thus causing re-renders to components that take it as an argument.然后你必须意识到回调将是一个新的 function 从而导致重新呈现将其作为参数的组件。
Is this ability to use useCallback to memoize the function considered a benefit of switching from useState to useReducer?
这种使用 useCallback 来记忆 function 的能力是否被认为是从 useState 切换到 useReducer 的好处?
No, not really.不,不是真的。
useCallback
does not prefer useState
or useReducer
. useCallback
不喜欢useState
或useReducer
。 As I said, useReducer
is really there to support a different style of programming, not because it provides functionality that is not available through other means.正如我所说,
useReducer
确实支持不同的编程风格,而不是因为它提供了其他方式无法提供的功能。
If I don't want to switch to useReducer, is there a way to use useCallback with useState in this case?
如果我不想切换到 useReducer,在这种情况下有没有办法将 useCallback 与 useState 一起使用?
Yes, as outlined above.是的,如上所述。
As mentioned by Gabriele Petrioli , You can use the callback syntax or you can keep the value of dependencies of your callback in a ref and use that ref in your callback instead of the state as mentioned here .正如Gabriele Petrioli所提到的,您可以使用回调语法,或者您可以将回调的依赖项值保存在 ref 中,并在回调中使用该 ref 而不是此处提到的 state。
In your example, this approach would look something like this:在您的示例中,这种方法看起来像这样:
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]
)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.