【问题标题】:`useState`, only update component when values in object change`useState`,仅在对象中的值更改时更新组件
【发布时间】:2020-01-13 17:08:10
【问题描述】:

问题

useState 总是触发更新,即使数据的值没有改变。

这是该问题的工作演示:demo

背景

我正在使用useState 挂钩来更新一个对象,并且我试图让它仅在该对象中的值发生变化时才更新。因为 React 使用Object.is 比较算法来确定何时应该更新;具有等效值的对象仍然会导致组件重新渲染,因为它们是不同的对象。

例如。即使有效载荷的值保持为{ foo: 'bar' },此组件也将始终重新渲染

const UseStateWithNewObject = () => {
  const [payload, setPayload] = useState({});
  useEffect(
    () => {
      setInterval(() => {
        setPayload({ foo: 'bar' });
      }, 500);
    },
    [setPayload]
  );

  renderCountNewObject += 1;
  return <h3>A new object, even with the same values, will always cause a render: {renderCountNewObject}</h3>;
};

问题

我是否可以用钩子实现类似shouldComponentUpdate 的东西来告诉react 只在数据更改时重新渲染我的组件?

【问题讨论】:

  • 您能否在useEffect 回调中添加您将在shouldComponentUpdate 中使用的逻辑,并且仅当新值与payload 不同时才调用setPayload

标签: javascript reactjs react-hooks


【解决方案1】:

我认为我们需要看到一个更好的现实生活示例来说明您要做什么,但从您分享的内容来看,我认为逻辑需要在状态设置之前向上游移动。

例如,您可以在更新状态之前手动比较 useEffect 中的传入值,因为这基本上是您询问 React 是否可以为您做的事情。

在这种情况下,有一个库 use-deep-compare-effect https://github.com/kentcdodds/use-deep-compare-effect 可能对您有用,它会处理大量涉及的手动工作,但即便如此,此解决方案仍假定开发人员将手动决定 (基于传入的道具等)是否应该更新状态。

例如:

const obj = {foo: 'bar'}
const [state, setState] = useState(obj)

useEffect(() => {
   // manually deep compare here before updating state
   if(obj.foo === state.foo) return
   setState(obj)
},[obj])

编辑:使用useRef 的示例,如果您不直接使用该值并且不需要组件基于它进行更新:

const obj = {foo: 'bar'}
const [state, setState] = useState(obj)

const { current: payload } = useRef(obj)

useEffect(() => {
    // always update the ref with the current value - won't affect renders
    payload = obj

    // Now manually deep compare here and only update the state if 
    //needed/you want a re render
    if(obj.foo === state.foo) return
    setState(obj)
},[obj])

【讨论】:

  • 在设置状态之前检查效果的相等性是个好主意!对我来说,这打破了关注点的分离。 useEffect 应该只担心保持状态更新。而组件应该确定它应该如何以及何时呈现。例如,如果我将此逻辑转换为自定义钩子,则另一个组件始终更新而另一个组件仅在发生更改时更新可能是有利的
  • 这是正确答案。如果你有一个 setInterval 做 setState({whatever}) 你总是将状态设置为一个新对象。这绝对应该触发重新渲染。把逻辑放在第一位,只有在数据发生变化时才返回一个新对象是处理这个问题的正确方法。
  • @CalIrvine 我不同意。我正在尝试做的是shouldComponentUpdate(nextProps, nextState) 存在于 React 中的原因。 payload 不使用作为输出的决定因素,因此它不应该导致重新渲染。我可以使用类组件或通过recompose 之类的工具来实现它,它会很好地工作。没有理由在这里混合担忧。
  • @agconti useEffect 的意思是替换组件生命周期事件,因此您可以在基于类的方法中对这些生命周期事件执行的任何操作都可以使用 useEffect 完成。我看到你来自不同的关注点,但是拥有多个useEffects 可能会解决这个问题。没有什么说您必须将所有逻辑都集中在一个效果中。
  • @CalIrvine 在我的演示中已经概述了一种更好的方法来执行您的建议。通过将对象视为带有JSON.stringify 的字符串,我可以让Object.is 仅更新更改的值。这可行,但由于前面提到的原因,我想找到另一种方法。
【解决方案2】:

我是否可以实现类似 shouldComponentUpdate 之类的带有钩子的东西,以告诉 react 仅在数据更改时重新渲染我的组件?

通常,对于状态更改,您在使用功能 useState 或使用 useRef 的引用之前与之前的值进行比较:

// functional useState
useEffect(() => {
  setInterval(() => {
    const curr = { foo: 'bar' };
    setPayload(prev => (isEqual(prev, curr) ? prev : curr));
  }, 500);
}, [setPayload]);
// with ref
const prev = useRef();
useEffect(() => {
  setInterval(() => {
    const curr = { foo: 'bar' };
    if (!isEqual(prev.current, curr)) {
      setPayload(curr);
    }
  }, 500);
}, [setPayload]);

useEffect(() => {
  prev.current = payload;
}, [payload]);

为了完整起见,“当数据更改时重新渲染我的组件?”也可能被称为道具,所以在这种情况下,你应该使用React.memo

如果你的函数组件在给定相同的 props 的情况下呈现相同的结果,你可以将它包装在对 React.memo 的调用中,以便在某些情况下通过记忆结果来提高性能。这意味着 React 将跳过渲染组件,并重用上次渲染的结果。

【讨论】:

    【解决方案3】:

    如果我理解得很好,您会尝试仅在状态的新值发生更改时调用setState,从而防止在未更改时进行不必要的重新渲染。

    如果是这种情况,您可以利用 useState 的回调形式

    const [state, setState] = useState({});
    setState(prevState => {
      // here check for equality and return prevState if the same
      return {...prevState, ...updatedValues};
    });
    

    这是一个自定义钩子(在 TypeScript 中),它会自动为您完成这项工作。它使用来自 lodash 的isEqual。但请随意用您认为合适的任何相等函数替换它。

    import { isEqual } from 'lodash';
    import { useState } from 'react';
    
    const useMemoizedState = <T>(initialValue: T): [T, (val: T) => void] => {
      const [state, _setState] = useState<T>(initialValue);
    
      const setState = (newState: T) => {
        _setState((prev) => {
          if (!isEqual(newState, prev)) {
            return newState;
          } else {
            return prev;
          }
        });
      };
    
      return [state, setState];
    };
    
    export default useMemoizedState;
    
    

    用法:

    const [value, setValue] = useMemoizedState('some initial value');
    

    【讨论】:

      【解决方案4】:

      不涉及向效果添加逻辑的通用解决方案是将组件拆分为:

      • 不受控制的容器,其状态呈现...
      • 已用React.memo 记忆的哑控无状态组件

      你的哑组件可以是纯的(就像它实现了shouldComponentUpdate 一样,你的智能状态处理组件可以是“哑”的,不用担心将状态更新为相同的值。

      例子:

      之前

      export default function Foo() {
        const [state, setState] = useState({ foo: "1" })
        const handler = useCallback(newValue => setState({ foo: newValue }))
      
        return (
          <div>
            <SomeWidget onEvent={handler} />
            Value: {{ state.foo }}
          </div>
        )
      

      之后

      const FooChild = React.memo(({foo, handler}) => {
        return (
          <div>
            <SomeWidget onEvent={handler} />
            Value: {{ state.foo }}
          </div>
        )
      })
      
      export default function Foo() {
        const [state, setState] = useState({ foo: "1" })
        const handler = useCallback(newValue => setState({ foo: newValue }))
      
        return <FooChild handler={handler} foo={state.foo} />
      }
      

      这为您提供了您正在寻找的逻辑的分离。

      【讨论】:

        【解决方案5】:

        您可以使用记忆化组件,它们只会在道具更改时重新渲染。

        const comparatorFunc = (prev, next) => {
          return prev.foo === next.foo
        }
        
        const MemoizedComponent = React.memo(({payload}) => {
            return (<div>{JSON.stringify(payload)}</div>)
        }, comparatorFunc);
        

        【讨论】:

        • 这里没有道具,只是陈述。来自文档“React.memo 不比较状态,因为没有要比较的单个状态对象。但是您也可以使孩子成为纯洁的,甚至可以使用 useMemo 优化单个孩子。”这是它不起作用的演示:codepen.io/agconti/pen/oNgypaV?editors=0011
        • @agconti 首先,你必须学习如何使用 React.memo,然后你必须自己澄清什么时候在 react 中实际渲染发生
        • 这里没有道具,这是正确的,UseStateWithNewObject 组件实际重新渲染的唯一原因是因为 renderCountNewObject 存在于 h3 内部。如果你删除它,你的控制台日志无论如何都会被调用,但这并不意味着每次都在渲染 DOM 节点。而且我从来没有说过你必须将 UseStateWithNewObject 包装在 React.memo 中。你必须优化子组件
        猜你喜欢
        • 2022-10-14
        • 1970-01-01
        • 1970-01-01
        • 2022-09-27
        • 2020-08-31
        • 2021-02-28
        • 2021-04-21
        • 2021-02-13
        • 1970-01-01
        相关资源
        最近更新 更多