【问题标题】:Usage of useCallback and setting new object state using previous state as argument使用 useCallback 并使用先前状态作为参数设置新对象状态
【发布时间】:2020-04-20 19:36:03
【问题描述】:

考虑这个带有自定义表单钩子的基本表单字段组件来处理输入更改:

import React, { useState, useCallback } from 'react';

const useFormInputs = (initialState = {})=> {
    const [values, setValues] = useState(initialState);
    const handleChange = useCallback(({ target: { name, value } }) => {
        setValues(prev => ({ ...prev, [name]: value }));
    }, []);
    const resetFields = useCallback(() =>
        setValues(initialState), [initialState]);
    return [values, handleChange, resetFields];
};

const formFields = [
    { name: 'text', placeholder: 'Enter text...', type: 'text', text: 'Text' },
    { name: 'amount', placeholder: 'Enter Amount...', type: 'number',
        text: 'Amount (negative - expense, positive - income)' }
];

export const AddTransaction = () => {
    const [values, handleChange, resetFields] = useFormInputs({
        text: '', amount: ''
    });
    return <>
        <h3>Add new transaction</h3>
        <form>
            {formFields.map(({ text, name, ...attributes }) => {
                const inputProps = { ...attributes, name };
                return <div key={name} className="form-control">
                    <label htmlFor={name}>{text}</label>
                    <input {...inputProps} value={values[name]}
                        onChange={handleChange} />
                </div>;
            })}
            <button className="btn">Add transaction</button>
        </form>
        <button className="btn" onClick={resetFields}>Reset fields</button>
    </>;
};
  1. 真的有任何理由/优势让我使用 useCallback 将函数缓存在我的自定义挂钩中吗?我阅读了文档,但我无法理解这种 useCallback 用法背后的想法。它究竟是如何记住渲染之间的函数的? ti 究竟是如何工作的,我应该使用它吗?

  2. 在同一个自定义钩子中,您可以看到通过传播先前的状态并创建一个新对象来更新新值状态,如下所示:setValues(prev =&gt; ({ ...prev, [name]: value })); 如果我这样做会有什么不同吗? setValues({ ...prev, [name]: value }) 据我所知,它看起来没有任何区别,对吧?我只是直接访问状态..我错了吗?

【问题讨论】:

    标签: javascript reactjs react-hooks


    【解决方案1】:

    你的第一个问题:

    在您的情况下,这并不重要,因为所有内容都在同一个组件中呈现。如果您有一个获取事件处理程序的事物列表,那么 useCallback 可以为您节省一些渲染。

    在下面的示例中,前 2 个项目是使用每次 App 重新呈现时重新创建的 onClick 呈现的。这不仅会导致 Items 重新渲染,还会导致虚拟 DOM 比较失败,React 会在 DOM 中重新创建 Itms(昂贵的操作)。

    最后 2 个项目获得一个 onClick,它在 App 挂载时创建,而不是在 App 重新渲染时重新创建,因此它们永远不会重新渲染。

    const { useState, useCallback, useRef, memo } = React;
    const Item = memo(function Item({ onClick, id }) {
      const rendered = useRef(0);
      rendered.current++;
      return (
        <button _id={id} onClick={onClick}>
          {id} : rendered {rendered.current} times
        </button>
      );
    });
    const App = () => {
      const [message, setMessage] = useState('');
      const onClick = (e) =>
        setMessage(
          'last clicked' + e.target.getAttribute('_id')
        );
      const memOnClick = useCallback(onClick, []);
    
      return (
        <div>
          <h3>{message}</h3>
          {[1, 2].map((id) => (
            <Item key={id} id={id} onClick={onClick} />
          ))}
          {[1, 2].map((id) => (
            <Item key={id} id={id} onClick={memOnClick} />
          ))}
        </div>
      );
    };
    
    ReactDOM.render(<App />, document.getElementById('root'));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
    
    
    <div id="root"></div>

    另一个例子是当你想在一个效果中调用一个函数时,它也需要在效果之外调用,所以你不能把这个函数放在效果里面。你只想在某个值发生变化时运行效果,这样你就可以做这样的事情。

    //fetchById is (re) created when ID changes
    const fetchById = useCallback(
      () => console.log('id is', ID),
      [ID]
    );
    //effect is run when fetchById changes so basically
    //  when ID changes
    useEffect(() => fetchById(), [fetchById]);
    

    你的第二个问题:

    setValues({ ...prev, [name]: value }) 会给您一个错误,因为您从未定义 pref 但如果您的意思是:setValues({ ...values, [name]: value }) 并将处理程序包装在 useCallback 中,那么现在您的回调依赖于 values 并且将不必要地重新创建每当值发生变化时。

    如果你不提供依赖,那么 linter 会警告你,你最终会得到一个stale closure。这是一个陈旧的闭包示例,因为 counter.count 永远不会增加,因为您在第一次渲染后永远不会重新创建 onClick,因此计数器闭包将始终为 {count:1}

    const { useState, useCallback, useRef } = React;
    const App = () => {
      const [counts, setCounts] = useState({ count: 1 });
      const rendered = useRef(0);
      rendered.current++;
      const onClick = useCallback(
        //this function is never re created so counts.count is always 1
        //  every time it'll do setCount(1+1) so after the first
        //  click this "stops working"
        () => setCounts({ count: counts.count + 1 }),
        [] //linter warns of missing dependency count
      );
      return (
        <button onClick={onClick}>
          count: {counts.count} rendered:{rendered.current}
        </button>
      );
    };
    
    ReactDOM.render(<App />, document.getElementById('root'));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
    
    
    <div id="root"></div>

    【讨论】:

    • 至于第二个问题,是的,我的意思是setValues({ ...values, [name]: value })。假设我没有使用 useCallback 来包装它,这和做 setValues(prev=&gt; ({...prev, [name]: value})) 有什么区别吗?
    • @kevkev 如果您想将setValues({ ...values, [name]: value }) 包装在useCallback 中,那么您不必要地创建一个依赖项values,忽略此依赖项将创建一个过时的闭包,就像上面的示例(反例)一样。如果您不将其包装在 useCallback 或任何其他记忆钩子中,则没有区别,但是您将不必要地重新渲染子组件。它不会在您的示例中,因为您没有子组件。
    猜你喜欢
    • 1970-01-01
    • 2019-08-23
    • 2021-12-02
    • 1970-01-01
    • 2020-11-27
    • 2019-10-20
    • 1970-01-01
    • 1970-01-01
    • 2019-04-19
    相关资源
    最近更新 更多