【问题标题】:React useState hook (and useEffect) not working in callback function反应 useState 钩子(和 useEffect)在回调函数中不起作用
【发布时间】:2021-10-16 12:01:36
【问题描述】:

已经尝试过 useState,它是功能变体并尝试使用 useEffect(不允许在回调函数中使用)。

我被困住了。
我有一个父“表”组件,它呈现一个 TableHead 孩子和一个 TableBody 孩子。 TableHead 子项有一个复选框,单击该复选框时会在父项上执行回调函数。 此时,布尔 selectAll(来自 useState 设置器和值)应该切换(更改值)。 但它仍处于初始状态。 结果是 selectall 的标题复选框第一次触发并且重新渲染确实将正文中的所有行显示为选中,但随后取消选中“selectAll”确实触发回调,但“selectAll”仍然为 false并且所有行都保持选中状态。

父组件代码:

function OTable(props) {

    const [selectAll, setSelectAll] = useState(false);

    const onAllRowsSelected = useCallback(() => {
       if (selectAll === false)
       {
            setSelectAll(selectAll => selectAll = true);
       }
       else
       {
           setSelectAll(selectAll => selectAll = false);
       }
   }

   return (
    <TableContainer>
        <Table>
            <OTableHead
                onSelectAllClick={onAllRowsSelected}
                setSelectAll={setSelectAll}
            />
            <OTableBody
                selectAll={selectAll}
            />
        </Table>
    </TableContainer>

怎么做? 谢谢

【问题讨论】:

    标签: reactjs react-hooks state


    【解决方案1】:

    如果我做了几个合理的假设(例如,您在 useCallback 调用上关闭了 )),则您切换 selectAll 的代码有效,但我怀疑您使用 useCallback它并不完全按照您想要的方式工作。这是您的代码与这些假设:

    const {useState, useCallback} = React;
    
    const TableContainer = ({children}) => {
        return <div>{children}</div>;
    };
    const Table = ({children}) => {
        return <div>{children}</div>;
    };
    const OTableHead = ({onSelectAllClick, children}) => {
        console.log(`OTableHead is rendering`);
        return <div>
            <input type="button" onClick={onSelectAllClick} value="Select All" />
            <div>{children}</div>
        </div>;
    };
    const OTableBody = ({selectAll}) => {
        return <div>selectAll = {String(selectAll)}</div>;
    };
    
    function OTable(props) {
    
        const [selectAll, setSelectAll] = useState(false);
    
        const onAllRowsSelected = useCallback(() => {
            if (selectAll === false)
            {
                setSelectAll(selectAll => selectAll = true);
            }
            else
            {
               setSelectAll(selectAll => selectAll = false);
           }
        });
    
        return (
            <TableContainer>
                <Table>
                    <OTableHead
                        onSelectAllClick={onAllRowsSelected}
                        setSelectAll={setSelectAll}
                    />
                    <OTableBody
                        selectAll={selectAll}
                    />
                </Table>
            </TableContainer>
        );
    }
    
    ReactDOM.render(
        <OTable />,
        document.getElementById("root")
    );
    <div id="root"></div>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

    但有些事情很突出:

    1. 您没有将任何依赖数组传递给useCallback。这没有任何用处,因为useCallback 将始终返回您传递给它的新函数。我怀疑你的意思是在它上面有一个空的依赖数组,以便它总是重用第一个函数(以避免不必要地重新渲染OTableHead)。

    2. 您使用的是setSelectAll 的回调形式,但您使用的是硬编码值(truefalse)。这段代码:

      const onAllRowsSelected = useCallback(() => {
          if (selectAll === false)
          {
               setSelectAll(selectAll => selectAll = true);
          }
          else
          {
              setSelectAll(selectAll => selectAll = false);
          }
      });
      

      完全符合这段代码的作用(假设我们知道selectAll 是一个布尔值,如果我们不知道它会非常微妙地不同):

      const onAllRowsSelected = useCallback(() => {
          setSelectAll(!selectAll);
      });
      

      因为if 使用函数关闭的selectAll 版本,而不是回调接收到的参数。 (setSelectAll(selectAll =&gt; selectAll = false); 在功能上与setSelectAll(() =&gt; false) 相同,分配给参数没有任何效果。)反过来,该代码与此相同:

      const onAllRowsSelected = () => {
          setSelectAll(!selectAll);
      };
      

      但我怀疑您使用回调版本的原因与使用 useCallback 的原因相同。

    代码未能成功避免重新渲染,正如您在上面添加到OTableHeadconsole.log 中看到的那样。

    useCallback 通过记忆回调来避免在回调没有真正改变的情况下重新渲染子元素。这是您在该代码中正确使用它的方法

    1. 将一个空的依赖数组传递给useCallback,因此它只返回您定义的第一个回调。

    2. 使用setSelectAll的函数版本传递给你的回调的参数值。

    3. 确保在回调未更改的情况下您想要的组件不会重新渲染,并对其属性进行检查,并且在它们未更改时不会重新渲染。使用像 OTableHead 这样的函数组件,您只需将其传递给 React.memo 即可。

    以下是上面的示例,其中包含这些更改:

    const {useState, useCallback} = React;
    
    const TableContainer = ({children}) => {
        return <div>{children}</div>;
    };
    const Table = ({children}) => {
        return <div>{children}</div>;
    };
    // *** Use `React.memo`:
    const OTableHead = React.memo(({onSelectAllClick, children}) => {
        console.log(`OTableHead is rendering`);
        return <div>
            <input type="button" onClick={onSelectAllClick} value="Select All" />
            <div>{children}</div>
        </div>;
    });
    const OTableBody = ({selectAll}) => {
        return <div>selectAll = {String(selectAll)}</div>;
    };
    
    function OTable(props) {
    
        const [selectAll, setSelectAll] = useState(false);
    
        const onAllRowsSelected = useCallback(() => {
            // Callback version, using the parameter value
            setSelectAll(selectAll => !selectAll);
        }, []); // <=== Empty dependency array
    
        return (
            <TableContainer>
                <Table>
                    <OTableHead
                        onSelectAllClick={onAllRowsSelected}
                        setSelectAll={setSelectAll}
                    />
                    <OTableBody
                        selectAll={selectAll}
                    />
                </Table>
            </TableContainer>
        );
    }
    
    ReactDOM.render(
        <OTable />,
        document.getElementById("root")
    );
    <div id="root"></div>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

    如果您担心不必要的重新渲染,那么您可以完全摆脱 useCallback 并执行以下操作:

    const onAllRowsSelected = () => {
        setSelectAll(!selectAll);
    };
    

    【讨论】:

    • 如果你不添加依赖到 useCallback 不应该总是返回相同的值?即使不需要依赖,因为您没有在 useCallback 中使用 selectAll。我很困惑...
    • @GiovanniEsposito - 你说得对,我在心里填充了一个不存在的空依赖数组。 OP 的代码虽然古怪,但应该可以工作(尽管它们可能并不意味着回调每次都会改变)。我会更新或删除,谢谢你的举报!
    • @GiovanniEsposito - 我已经修好了,再次感谢。
    【解决方案2】:

    不需要记忆状态更新函数,因为 React 保证它是一个稳定的引用。

    useState

    注意

    React 保证 setState 函数的身份是稳定的,不会 更改重新渲染。这就是为什么从 useEffect 中省略是安全的 或者 useCallback 依赖列表。

    您可以简化回调,只使用功能更新来更新状态。

    const onAllRowsSelected = () => setSelectAll(all => !all);
    

    如果OTableHead 要求onSelectAllClick 属性是一个稳定的引用,那么useCallback 可以与一个空的依赖数组一起使用,以提供一个稳定的onAllRowsSelected 回调引用。 注意:这不会影响onAllRowsSelected 从之前的状态值正确切换的能力,它只是为子组件提供一个稳定的回调引用。 p>

    useCallback

    useCallback 将返回回调的记忆版本,仅 如果依赖项之一发生更改,则更改。 这在以下情况下很有用 将回调传递给依赖于引用的优化子组件 平等以防止不必要的渲染(例如 应该组件更新)。

    const onAllRowsSelected = useCallback(
      () => setSelectAll(all => !all),
      [],
    );
    

    【讨论】:

    • 好的,useState 部分很清楚。但是,如果您不将依赖项添加到 useCallback 不应该总是返回相同的值吗?即使依赖项不是必需的,因为您没有在 useCallback 中使用 selectAll。我错过了什么?
    • @GiovanniEsposito 这就是记忆回调的想法,因此它始终是对相同回调函数的引用,而不是在每个渲染周期都重新声明。如果它被传递给孩子并且它是一个新的引用,那么这可能会触发不必要的重新渲染。
    • 好的,有趣,谢谢,.. 如果我想查询'selectAll' 中的值?如果 setSelect All 不是布尔值,而是说一个整数(比如能够在 0 和 1 值之间切换,那么 preState 是否需要更改?
    • @NealRogers 如果您想在两个值之间切换,请使用此模式setValue(value =&gt; value === &lt;value1&gt; ? &lt;value2&gt; : &lt;value1&gt;)。函数式更新只是一个函数,它将上一个状态值作为参数,只需要返回下一个状态值,你可以在这两点之间应用任何逻辑。
    【解决方案3】:

    是的,保留初始值,因为useCallback 是一个记忆,如果您不添加状态依赖项,它会保留初始值(由于记忆本身)。要解决,只需将selectAll 作为useCallback 依赖:

    const onAllRowsSelected = useCallback(() => {
       setSelectAll((prev) => !prev)
    }, [selectAll])
    

    【讨论】:

      【解决方案4】:

      在这种情况下,您不需要 useCalback。只需像这样更新 setState:

      const onAllRowsSelected = () => {
        setSelectAll((preState) => !preState);
      };
      

      【讨论】:

      • useCallback 记忆函数,使其不会不断变化,从而触发 OTableHead 组件的重新渲染。所以拥有它肯定很有用,他们只需要做你在函数体中所做的事情:使用setSelectAll回调接收的参数。
      • 主要问题是关于更新状态逻辑。他不了解setState,所以不应该谈论useCallback
      • 我认为这不是主要问题,没有useCallback就没有必要使用setSelectAll的回调版本。 OP 的代码将成功切换selectAll,尽管可能不是他们想要的方式。我认为useCallback 的使用和setSelectAll 的回调版本对这个问题很重要,例如,尽量避免重新渲染像OTableHead 这样的孩子。
      • 有经验的人,没关系。但是为什么你认为使用useCallback 对甚至不了解setState 的人很重要?
      • 我没有。我的观点是,我认为 OP 使用它是有原因的。但他们可能只是感到困惑。不过,在这种情况下,如果您不使用useCallback,则不需要setSelectAll 的回调形式。无论如何,希望OP会回来澄清。编码愉快!
      猜你喜欢
      • 2021-04-10
      • 2021-09-06
      • 1970-01-01
      • 2019-10-07
      • 2021-05-26
      • 1970-01-01
      • 2020-12-30
      • 2020-09-21
      • 1970-01-01
      相关资源
      最近更新 更多