使用 useCallback 并使用先前状态作为参数设置新对象状态

2024-05-26

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

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 => ({ ...prev, [name]: value }));如果我这样做会有什么不同吗?setValues({ ...prev, [name]: value })据我所知,看起来没有什么区别,对吗?我只是直接访问状态..我错了吗?


你的第一个问题:

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

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

最后 2 个项目获得一个 onClick,该 onClick 在应用程序安装时创建,但在应用程序重新渲染时不会重新创建,因此它们永远不会重新渲染。

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]);

你的第二个问题:

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

如果您不提供依赖项,那么 linter 会警告您,并且您最终会得到一个陈旧的封闭 https://dmitripavlutin.com/react-hooks-stale-closures/。这是陈旧闭包的示例,因为 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>
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用 useCallback 并使用先前状态作为参数设置新对象状态 的相关文章

随机推荐