使用 useCallback 并使用以前的状态作为参数设置新的对象状态 [英] Usage of useCallback and setting new object state using previous state as argument

查看:40
本文介绍了使用 useCallback 并使用以前的状态作为参数设置新的对象状态的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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]);返回 [值,handleChange,resetFields];};const formFields = [{ name: 'text', placeholder: 'Enter text...', type: 'text', text: 'Text' },{ name: 'amount', placeholder: 'Enter Amount...', type: 'number',文本:'金额(负 - 费用,正 - 收入)' }];export const AddTransaction = () =>{const [values, handleChange, resetFields] = useFormInputs({文字:'',金额:''});返回 <><h3>添加新交易</h3><表格>{formFields.map(({ text, name, ...attributes }) => {const inputProps = { ...属性,名称};return 

<label htmlFor={name}>{text}</label><input {...inputProps} value={values[name]}onChange={handleChange}/>

;})}<button className="btn">添加交易</button></表单><button className="btn" onClick={resetFields}>重置字段</button></>;};

  1. 我真的有什么理由/优势可以使用 useCallback 在我的自定义钩子中缓存函数吗?我阅读了文档,但我只是无法理解 useCallback 的这种用法背后的想法.它究竟是如何记住渲染之间的功能的?ti 究竟是如何工作的,我应该使用它吗?

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

解决方案

您的第一个问题:

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

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

最后 2 个项目获得一个 onClick,它是在应用挂载时创建的,而不是在应用重新呈现时重新创建,因此它们永远不会重新呈现.

const { useState, useCallback, useRef, memo } = React;const Item = memo(function Item({ onClick, id }) {const 渲染 = useRef(0);渲染.current++;返回 (<button _id={id} onClick={onClick}>{id} :呈现 {rendered.current} 次);});const App = () =>{const [message, setMessage] = useState('');const onClick = (e) =>设置消息('上次点击' + e.target.getAttribute('_id'));const memOnClick = useCallback(onClick, []);返回 (<div><h3>{消息}</h3>{[1, 2].map((id) => (<项目键={id} id={id} onClick={onClick}/>))}{[1, 2].map((id) => (<项目键={id} id={id} onClick={memOnClick}/>))}

);};ReactDOM.render(, 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 在 ID 改变时(重新)创建const fetchById = useCallback(() =>console.log('id is', ID),[ID]);//当 fetchById 改变时运行效果,所以基本上//当 ID 改变时useEffect(() => fetchById(), [fetchById]);

你的第二个问题:

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

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

const { useState, useCallback, useRef } = React;const App = () =>{const [counts, setCounts] = useState({ count: 1 });const 渲染 = useRef(0);渲染.current++;const onClick = useCallback(//此函数永远不会重新创建,因此counts.count 始终为1//每次它都会执行 setCount(1+1) 所以在第一个之后//点击这个停止工作"() =>setCounts({ count: counts.count + 1 }),[]//linter 警告缺少依赖项计数);返回 (<button onClick={onClick}>计数:{counts.count} 渲染:{rendered.current});};ReactDOM.render(, 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>

Consider this basic form fields component with a custom form hook to handle input changes:

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. Is there really any reason / advantage for me to use useCallback to cache the function in my custom hook? I read the docs, but I just coudln't grasp the idea behind this usage of useCallback. How exactly it memoizes the function between renders? How exactly does ti work, and should I use it?

  2. Inside the same custom hook, you can see the new values state being updated by spreading the previous state and creating a new object like so: setValues(prev => ({ ...prev, [name]: value })); Would there be any difference if I did this instead? setValues({ ...prev, [name]: value }) as far as I can tell, doesn't look like it has any difference right? I am simply accessing the state directly.. Am I wrong?

解决方案

Your first question:

In your case it doesn't matter because everything is rendered in the same component. If you have a list of things that get an event handler then useCallback can save you some renders.

In the example below the first 2 items are rendered with an onClick that is re created every time App re renders. This will not only cause the Items to re render it will also cause virtual DOM compare to fail and React will re create the Itms in the DOM (expensive operation).

The last 2 items get an onClick that is created when App mounts and not re created when App re renders so they will never re render.

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>

Another example is when you want to call a function in an effect that also needs to be called outside of the effect so you can't put the function inside the effect. You only want to run the effect when a certain value changes so you can do something like this.

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

Your second question:

The setValues({ ...prev, [name]: value }) will give you an error because you never defined pref but if you meant: setValues({ ...values, [name]: value }) and wrap the handler in a useCallback then now your callback has a dependency on values and will be needlessly be re created whenever values change.

If you don't provide the dependency then the linter will warn you and you end up with a stale closure. Here is an example of the stale closure as counter.count will never go up because you never re create onClick after the first render thus the counter closure will always be {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>

这篇关于使用 useCallback 并使用以前的状态作为参数设置新的对象状态的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆