【问题标题】:How to use most recent state from useReducer before re-render如何在重新渲染之前使用来自 useReducer 的最新状态
【发布时间】:2022-01-07 16:03:54
【问题描述】:

我有两个减速器动作,我想一个接一个地调度。第一个修改状态,然后第二个使用修改后的状态的一部分进行另一次修改。困难在于,当调用第二次调度时,它仍然具有旧的过时状态,因此无法正确更新状态。

下面是一个示例(也可以在此处找到 - https://codesandbox.io/s/react-usereducer-hqtc2),其中有一个对话列表以及一个被视为“活动”对话的注释:

import React, { useReducer } from "react";

const reducer = (state, action) => {
  switch (action.type) {
    case "removeConversation":
      return {
        ...state,
        conversations: state.conversations.filter(
          c => c.title !== action.payload
        )
      };
    case "setActive":
      return {
        ...state,
        activeConversation: action.payload
      };
    default:
      return state;
  }
};

export default function Conversations() {
  const [{ conversations, activeConversation }, dispatch] = useReducer(
    reducer,
    {
      conversations: [
        { title: "James" },
        { title: "John" },
        { title: "Mindy" }
      ],
      activeConversation: { title: "James" }
    }
  );

  function removeConversation() {
    dispatch({ type: "removeConversation", payload: activeConversation.title });
    dispatch({ type: "setActive", payload: conversations[0] });
  }

  return (
    <div>
      Active conversation: {activeConversation.title}
      <button onClick={removeConversation}>Remove</button>
      <ul>
        {conversations.map(conversation => (
          <li key={conversation.title}>{conversation.title}</li>
        ))}
      </ul>
    </div>
  );
}

在这里,当我单击“删除对话”按钮时,我想删除活动对话,然后将活动对话设置为列表顶部的那个。然而,在这里,当第一个调度从列表中删除对话时,第二个调度将 active 设置为conversations[0],它仍然包含删除的值(因为状态尚未更新)。因此,即使它已从列表中删除,它也会将活动对话保持在之前的状态。

我可能可以将逻辑组合成一个动作并在那里完成所有操作(删除对话并将所有设置为活动状态),但我希望尽可能让我的 reducer 动作各有一个责任。

有什么办法可以让第二个 dispatch call 有最新版本的状态,这样就不会出现这种问题了?

【问题讨论】:

    标签: react-hooks


    【解决方案1】:

    如果您将useEffect() 视为setState 的第二个参数(来自基于类的组件),它可能会有所帮助。

    如果你想用最近的状态进行操作,使用useEffect(),当状态改变时会被命中:

    const {
      useState,
      useEffect
    } = React;
    
    function App() {
      const [count, setCount] = useState(0);
      const decrement = () => setCount(count-1);
      const increment = () => setCount(count+1);
      
      useEffect(() => {
        console.log("useEffect", count);
      }, [count]);
      console.log("render", count);
      
      return ( 
        <div className="App">
          <p>{count}</p> 
          <button onClick={decrement}>-</button> 
          <button onClick={increment}>+</button> 
        </div>
      );
    }
    
    const rootElement = document.getElementById("root");
    ReactDOM.render( < App / > , rootElement);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
    
    <div id="root"></div>

    Some further info on useEffect()

    【讨论】:

      【解决方案2】:

      为将来可能遇到类似问题的任何人解答此问题。找到解决方案的关键是了解state in React is a snapshot

      可以看到在调度的setActive动作中,状态的conversations[0]的值正在被传递:

      dispatch({ type: "setActive", payload: conversations[0] });

      因此,当在下一次渲染之前调用该操作时,它会使用重新渲染时的快照状态:

      // snapshot of state when action is called
      {
        conversations: [
          { title: "James" },
          { title: "John" },
          { title: "Mindy" }
        ],
        activeConversation: { title: "James" }
      }
      

      因此conversations[0] 的计算结果为{title: "James"}。这就是为什么在 reducer 中,activeConversation: action.payload 返回 {title: "James"} 并且活动对话不会改变。在技​​术方面,"you're calculating the new state from the value in your closure, instead of calculating it from the most recent value."

      那么我们如何解决这个问题?好吧,useReducer 实际上总是可以访问最新的状态值。它是state updater functionsister pattern,即使在下一次渲染之前,您也可以访问最新的状态变量。

      这意味着在第一个 dispatch action 之后:

      dispatch({ type: "removeConversation", payload: activeConversation.title }); // first dispatch action
      dispatch({ type: "setActive", payload: conversations[0] }); // second dispatch action
      

      下一个调度动作实际上已经可以访问最新状态。您只需要访问它:

      case "setActive":
        return {
          ...state,
          activeConversation: state.conversations[0]
        };
      

      您可以通过将其记录到控制台来验证这一点:

      const reducer = (state, action) => {
        console.log(state);
        switch (action.type) {
          case "removeConversation":
            return {
              ...state,
              conversations: state.conversations.filter(
                c => c.title !== action.payload
              )
            };
          case "setActive":
            return {
              ...state,
              activeConversation: state.conversations[0]
            };
          default:
            return state;
        }
      };
      

      另外需要注意的是,2 个调度调用是按上述状态更新程序函数链接中的说明进行批处理的。更多关于batching的信息也在这里。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-01-18
        • 2023-03-10
        • 2020-03-04
        • 2019-08-04
        • 2018-05-27
        • 2019-09-20
        • 2020-02-18
        • 1970-01-01
        相关资源
        最近更新 更多