简体   繁体   English

使用 `useCallback` 只能与 `useReducer` 一起使用?

[英]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 ,在这种情况下有没有办法将useCallbackuseState一起使用?

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不喜欢useStateuseReducer 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM