在 useEffect 2nd param 中使用对象而不必将其字符串化为 JSON

IT技术 javascript json reactjs react-hooks
2021-03-23 19:18:37

在 JS 中,两个对象不相等。

const a = {}, b = {};
console.log(a === b);

所以我不能在useEffect(React hooks)中使用一个对象作为第二个参数,因为它总是被认为是假的(所以它会重新渲染):

function MyComponent() {
  // ...
  useEffect(() => {
    // do something
  }, [myObject]) // <- this is the object that can change.
}

这样做(上面的代码),每次组件重新渲染时都会产生运行效果,因为每次都认为对象不相等。

我可以通过将对象作为 JSON 字符串化值传递来“破解”它,但这有点脏 IMO:

function MyComponent() {
  // ...
  useEffect(() => {
    // do something
  }, [JSON.stringify(myObject)]) // <- yuck

有没有更好的方法来做到这一点并避免不必要的效果调用?

旁注:对象具有嵌套属性。效果必须在此对象内的每个更改上运行。

3个回答

您可以创建一个自定义钩子来跟踪 a 中的先前依赖项数组ref并将对象与例如 Lodash 进行比较,isEqual并且仅在它们不相等时才运行提供的函数。

例子

const { useState, useEffect, useRef } = React;
const { isEqual } = _;

function useDeepEffect(fn, deps) {
  const isFirst = useRef(true);
  const prevDeps = useRef(deps);

  useEffect(() => {
    const isFirstEffect = isFirst.current;
    const isSame = prevDeps.current.every((obj, index) =>
      isEqual(obj, deps[index])
    );

    isFirst.current = false;
    prevDeps.current = deps;

    if (isFirstEffect || !isSame) {
      return fn();
    }
  }, deps);
}

function App() {
  const [state, setState] = useState({ foo: "foo" });

  useEffect(() => {
    setTimeout(() => setState({ foo: "foo" }), 1000);
    setTimeout(() => setState({ foo: "bar" }), 2000);
  }, []);

  useDeepEffect(() => {
    console.log("State changed!");
  }, [state]);

  return <div>{JSON.stringify(state)}</div>;
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>

这是一个非常酷的答案,感谢分享。问题虽然为什么不使用useState而不是useRef.
2021-06-12 19:18:37
@ThomasValadez 通过使用从useState组件返回的函数将被重新渲染,这在这种情况下是不希望的。
2021-06-13 19:18:37
嗨@Tholle,非常好的例子。只想问一件事。你展示的这个例子,我认为它可以开箱即用,无需任何_isEqual方法检查,因为对象不是嵌套的。我认为我们有这个问题,只有嵌套对象。如果我错了请纠正我?
2021-06-19 19:18:37

@Tholle 的上述回答是完全正确的。2 天前,我在 dev.to(https://dev.to/w1n5rx/ways-to-handle-deep-object-comparison-in-useeffect-hook-1elm上写了一篇关于相同的帖子

在 React 中,可以使用useEffect hook在功能组件中处理副作用在这篇文章中,我将讨论保存我们的 props/state 的依赖数组,特别是如果这个数组中有一个对象会发生什么。

useEffect即使依赖数组中的一个元素发生了变化钩子也会运行。React 这样做是为了优化目的。另一方面,如果您传递一个空数组,则它永远不会重新运行。

但是,如果这个数组中存在一个对象,事情就会变得复杂。那么即使对象被修改,钩子也不会重新运行,因为它不会对对象的这些依赖项更改之间进行深度对象比较。有几种方法可以解决这个问题。

  1. 使用 lodash 的isEqual 方法usePrevious hook这个钩子在内部使用一个 ref 对象,该对象包含一个current可以保存值的可变属性。

    未来 React 可能会提供一个开箱即用的 usePrevious Hook,因为它是一个相对常见的用例。

    const prevDeeplyNestedObject = usePrevious(deeplyNestedObject)
    useEffect(()=>{
        if (
            !_.isEqual(
                prevDeeplyNestedObject,
                deeplyNestedObject,
            )
        ) {
            // ...execute your code
        }
    },[deeplyNestedObject, prevDeeplyNestedObject])
    
  2. 使用useDeepCompareEffect 挂钩作为useEffect对象挂钩的替代

    import useDeepCompareEffect from 'use-deep-compare-effect'
    ...
    useDeepCompareEffect(()=>{
        // ...execute your code
    }, [deeplyNestedObject])
    
  3. 使用类似于解决方案#2 的useCustomCompareEffect 钩子

我准备了一个与这篇文章相关的 CodeSandbox示例分叉它并自己检查。

依赖数组中的普通(非嵌套)对象

我只想挑战这两个答案,并询问如果object依赖数组没有嵌套会发生什么如果那是没有更深层次属性的普通对象。

在我看来,在这种情况下,useEffect功能无需任何额外检查即可工作。

我只是想写这个,如果我错了,我想学习并更好地向自己解释。任何建议,解释是非常受欢迎的。

这里可能更容易检查和播放示例:https : //codesandbox.io/s/usehooks-bt9j5?file=/src/App.js

const {useState, useEffect} = React;

function ChildApp({ person }) {
  useEffect(() => {
    console.log("useEffect ");
  }, [person]);

  console.log("Child");
  return (
    <div>
      <hr />
      <h2>Inside child</h2>
      <div>{person.name}</div>
      <div>{person.age}</div>
    </div>
  );
}
function App() {
  const [person, setPerson] = useState({ name: "Bobi", age: 29 });
  const [car, setCar] = useState("Volvo");

  function handleChange(e) {
    const variable = e.target.name;
    setPerson({ ...person, [variable]: e.target.value });
  }

  function handleCarChange(e) {
    setCar(e.target.value);
  }

  return (
    <div className="App">
      Name:
      <input
        name="name"
        onChange={(e) => handleChange(e)}
        value={person.name}
      />
      <br />
      Age:
      <input name="age" onChange={(e) => handleChange(e)} value={person.age} />
      <br />
      Car: <input name="car" onChange={(e) => handleCarChange(e)} value={car} />
      <ChildApp person={person} />
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-
dom.development.js"></script>
<div id="root"></div>