React useEffect 对象比较

160

我正在使用React useEffect 钩子,检查对象是否发生了更改,只有在对象发生更改时才再次运行钩子。

我的代码如下所示。

const useExample = (apiOptions) => {
    const [data, updateData] = useState([]);
    useEffect(() => {
       const [data, updateData] = useState<any>([]);
        doSomethingCool(apiOptions).then(res => {               
           updateData(response.data);
       })
    }, [apiOptions]);

    return {
        data
    };
};

很不幸,由于对象未被识别为相同对象,它会持续运行。

我认为以下是一个例子说明这个问题。

const objA = {
   method: 'GET'
}

const objB = {
   method: 'GET'
}

console.log(objA === objB)

或许运行 JSON.stringify(apiOptions) 可以解决问题?


1
这个问题有解决方案吗?你没有选择答案,所以我想知道你是不是采取了其他方法? - FabricioG
@FabricioG 无法回忆起来,但是下面似乎有很多好的答案。 - peter flanagan
7个回答

104

使用apiOptions作为状态值

我不确定您如何使用自定义钩子,但是通过使用useStateapiOptions作为状态值应该可以正常工作。这样,您就可以将其作为状态值提供给自定义钩子,如下所示:

const [apiOptions, setApiOptions] = useState({ a: 1 })
const { data } = useExample(apiOptions)

只有在使用setApiOptions时,它才会改变。

示例#1

import { useState, useEffect } from 'react';

const useExample = (apiOptions) => {
  const [data, updateData] = useState([]);
  
  useEffect(() => {
    console.log('effect triggered')
  }, [apiOptions]);

  return {
    data
  };
}
export default function App() {
  const [apiOptions, setApiOptions] = useState({ a: 1 })
  const { data } = useExample(apiOptions);
  const [somethingElse, setSomethingElse] = useState('default state')

  return <div>
    <button onClick={() => { setApiOptions({ a: 1 }) }}>change apiOptions</button>
    <button onClick={() => { setSomethingElse('state') }}>
      change something else to force rerender
    </button>
  </div>;
}

或者

你可以编写一个深度可比较的useEffect,如此处所述:

function deepCompareEquals(a, b){
  // TODO: implement deep comparison here
  // something like lodash
  // return _.isEqual(a, b);
}

function useDeepCompareMemoize(value) {
  const ref = useRef() 
  // it can be done by using useMemo as well
  // but useRef is rather cleaner and easier

  if (!deepCompareEquals(value, ref.current)) {
    ref.current = value
  }

  return ref.current
}

function useDeepCompareEffect(callback, dependencies) {
  useEffect(
    callback,
    dependencies.map(useDeepCompareMemoize)
  )
}

你可以像使用 useEffect 一样使用它。


5
谢谢你的帮助,但我不想采取这种方法,因为可能有许多 API 选项。我已经通过使用JSON.stringify解决了这个问题,但是看起来在这里有更好的方法,不过我无法访问 egghead :-( https://dev59.com/K1QJ5IYBdhLWcg3wkGoq - peter flanagan
1
@peterflanagan,Egghead视频(我也没有访问权限)描述了一个带有深度比较的useEffect钩子的实现。我已经更新了我的答案并提供了一个提示,希望它能帮到你,但你必须使用lodash或其他适合你的比较算法来实现,如果你不能使用第三方库,你可以自己实现。 - lenilsondc
@peterflanagan,使用基于类的组件并使用didComponentUpdate是否更好?JSON.stringify可能会使性能提升无效,而复杂比较可能会使函数式组件的可读性降低。 - skyboyer
@lenilsondc 对于替代情况,将数组传递给useEffect依赖项非常重要 - 如果传递对象,则不会按预期工作。 - Krzysztof Cieslinski
@Alex,我做了一些调整,它是否按照你的预期工作或者有任何代码问题?已经过了一段时间,请耐心等待:D - lenilsondc
显示剩余2条评论

27

我刚找到了适合我的解决方案。

你需要使用 Lodash 的 usePrevious()_.isEqual()。 在 useEffect() 内部,你可以设置一个条件:如果之前的apiOptions等于当前的apiOptions,那么不做任何操作。如果不相等,则更新数据(updateData())

例如:

const useExample = (apiOptions) => {

     const myPreviousState = usePrevious(apiOptions);
     const [data, updateData] = useState([]);
     useEffect(() => {
        if (myPreviousState && !_.isEqual(myPreviousState, apiOptions)) {
          updateData(apiOptions);
        }
     }, [apiOptions])
}

usePrevious(value) 是一个自定义 Hook,它使用 useRef() 创建了一个 ref

你可以在 官方 React Hook 文档 中找到它。

const usePrevious = value => {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};

5
它是否会收到有关缺少依赖项“myPreviousState”的警告? - Up209d

26
如果输入的数据足够浅,你认为使用深层次的相等判断仍然很快,请考虑使用JSON.stringify
const useExample = (apiOptions) => {
    const [data, updateData] = useState([]);
    const apiOptionsJsonString = JSON.stringify(apiOptions);

    useEffect(() => {
       const apiOptionsObject = JSON.parse(apiOptionsJsonString);
       doSomethingCool(apiOptionsObject).then(response => {               
           updateData(response.data);
       })
    }, [apiOptionsJsonString]);

    return {
        data
    };
};

请注意它不会比较函数。


3
apiOptions不是太复杂的数据对象时,这似乎是一个不错且简单的解决方案。但我想知道:在useEffect()内部是否有必要执行JSON.parse(apiOptionsJsonString)?难道你不能直接使用来自父级作用域的apiOptions吗? - marcvangend
3
@marcvangend 如果您使用父范围中的 apiOptions,则必须将其添加为 useEffect 的依赖项,这将带回问题。 - adl

19

这是正确的答案。useDeepCompareEffect也是React Testing Library主要工程师开发的。此外,useDeepCompareEffect中的代码非常简洁易懂。我强烈建议阅读它。 - crazyGuy
1
欢迎来到SO!我们非常感谢您的帮助和回答问题。也许更好的方法是更详细地解释链接中的内容,并将其作为参考。 - AncientSwordRage

2

在某些情况下,这真的很简单!

const objA = {
   method: 'GET'
}

const objB = {
   method: 'GET'
}

console.log(objA === objB) // false

为什么objA不等于objB?因为JS只比较它们的地址,这两个对象是不同的。这是我们都知道的!

React hooks也是一样的!

所以,如我们所知,objA.method === objB.method是成立的。因为它们是字面值。

问题的答案就出来了:

React.useEffect(() => {
    // do your fancy work
}, [obj.method])

1
如果我不知道对象中可用的键,我该怎么做?我的对象可能有可变的键。 - Arashdeep Singh
如果您想查看对象的.id是否已更改以重新加载,而不重新加载其他属性,则此功能非常有用。 - KeitelDOG
只需使用JSON.stringify @Arashdeep - A.Chan

0

4
欢迎来到SO!我们非常感谢您的帮助和回答问题。也许更好的方法是详细解释链接中的内容,并将其作为参考。 - El.Hum

0

如果您有修改 doSomethingCool 的能力,另一个选择是:

如果您确切地知道需要哪些非 Object 属性,可以将依赖项列表限制为 useEffect 可以正确解释的属性,例如:

const useExample = (apiOptions) => {
    const [data, updateData] = useState([]);
    useEffect(() => {
       const [data, updateData] = useState<any>([]);
        doSomethingCool(apiOptions.method).then(res => {               
           updateData(response.data);
       })
    }, [apiOptions.method]);

    return {
        data
    };
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接