159

I am using react useEffect hooks and checking if an object has changed and only then run the hook again.

My code looks like this.

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

    return {
        data
    };
};

Unfortunately it keeps running as the objects are not being recognised as being the same.

I believe the following is an example of why.

const objA = {
   method: 'GET'
}

const objB = {
   method: 'GET'
}

console.log(objA === objB)

Perhaps running JSON.stringify(apiOptions) works?

skyboyer
  • 22,209
  • 7
  • 57
  • 64
peter flanagan
  • 9,195
  • 26
  • 73
  • 127

7 Answers7

103

Use apiOptions as state value

I'm not sure how you are consuming the custom hook but making apiOptions a state value by using useState should work just fine. This way you can serve it to your custom hook as a state value like so:

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

This way it's going to change only when you use setApiOptions.

Example #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>;
}

Alternatively

You could write a deep comparable useEffect as described here:

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)
  )
}

You can use it like you'd use useEffect.

lenilsondc
  • 9,590
  • 2
  • 25
  • 40
  • 5
    Thanks for your help, but I don't want to take this approach as there could be many API options. I have solved this by using `JSON.stringify` but looks like there is a better way to do it described here but I don't have access to egghead :-( https://stackoverflow.com/questions/53601931/custom-useeffect-second-argument – peter flanagan Jan 08 '19 at 17:02
  • 1
    @peterflanagan the egghead video (I don't have access as well) describe an implementation of a `useEffect` hook with deep comparison. I've updated my answer with a hint, I hope it helps, but you have to implement the comparison algorithm that is most suitable for you using lodash or something else should do the job but if you can't use 3rd party libs you can implement you own. – lenilsondc Jan 08 '19 at 17:52
  • @peterflanagan might it be better just to switch onto class-based component with `didComponentUpdate` instead? `JSON.stringify` may nullify any performance boost while complex comparison may nullify better readability of functional components. – skyboyer Jan 12 '19 at 19:58
  • @lenilsondc for the alternative case its important to pass the array to useEffect dependencies - if you pass an object then it won't work as expected. – Krzysztof Cieslinski May 09 '20 at 11:30
  • @lenilsondc In the alternative method, there is no array passed to useEffect. Can you explain how this works? When I tried using that approach, ESLint complained that I was passing a function as second argument to useEffect instead of an array. – Alex Oct 22 '20 at 15:12
  • @Alex I've made a few adjustments, is it working as you'd expect or there are any issues with that code? It's been some time so bare with me :D – lenilsondc Oct 22 '20 at 21:25
  • using that dependencies.map(useDeepCompareMemoize) can change the number of hooks called, if dependencies count change. You'd better use useDeepCompareMemoize(dependencies) – nemenos Mar 24 '22 at 19:42
27

I just found a solution which works for me.

You have to use usePrevious() and _.isEqual() from Lodash. Inside the useEffect(), you put a condition if the previous apiOptions equals to the current apiOptions. If true, do nothing. If false updateData().

Example :

const useExample = (apiOptions) => {

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

usePrevious(value) is a custom hook which create a ref with useRef().

You can found it from the Official React Hook documentation.

const usePrevious = value => {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};
tanguy_k
  • 11,307
  • 6
  • 54
  • 58
Steffi
  • 6,835
  • 25
  • 78
  • 123
25

If the input is shallow enough that you think deep equality would still be fast, consider using 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
    };
};

Note it won’t compare functions.

adl
  • 1,865
  • 1
  • 25
  • 32
  • 3
    This seems like a nice and simple solution when `apiOptions` is a not-too-complex data object. But I wonder: is it necessary to do `JSON.parse(apiOptionsJsonString)` inside useEffect()? Can't you simply use `apiOptions` from the parent scope? – marcvangend Aug 28 '20 at 08:34
  • 2
    @marcvangend if you use the `apiOptions` from the parent scope you would have to add it as a dependency to the useEffect which will bring back the problem.. – adl Aug 29 '20 at 17:50
18

If you're real sure that you cannot control apiOptions then just replace native useEffect with https://github.com/kentcdodds/use-deep-compare-effect.

kasongoyo
  • 1,748
  • 1
  • 14
  • 20
  • This is the correct answer. The `useDeepCompareEffect` is also developed by the main engineer behind the React Testing Library. Further, the code in `useDeepCompareEffect` very small and simple to understand. I highly suggest to read it. – crazyGuy Aug 11 '20 at 12:15
  • 1
    Welcome to the SO! we really appreciate your help and answering the questions. Maybe it is better to explain more about what is in the link and just use it as a reference. – AncientSwordRage Feb 23 '22 at 12:35
1

It's really so simple in some cases!

const objA = {
   method: 'GET'
}

const objB = {
   method: 'GET'
}

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

Why objA not equal with objB? Coz JS just compare their address right? They are two different obj. That's we all know!

The same as React hooks does!

So, also as we all know, objA.method === objB.method right? Because they are literal.

The answer comes out:

React.useEffect(() => {
    // do your fancy work
}, [obj.method])
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
A.Chan
  • 620
  • 6
  • 7
0

You can use useDeepCompareEffect, useCustomCompareEffect or write your own hook.

https://github.com/kentcdodds/use-deep-compare-effect https://github.com/sanjagh/use-custom-compare-effect

Volatore74
  • 17
  • 1
  • 4
    Welcome to the SO! we really appreciate your help and answering the questions. Maybe it is better to explain more about what is in the link and just use it as a reference. – El.Hum Sep 19 '20 at 05:38
0

One other option, if you have the ability to modify doSomethingCool:

If know exactly which non-Object properties you need, you can limit the list of dependencies to properties that useEffect will correctly interpret with ===, e.g.:

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
    };
};
accidental_PhD
  • 744
  • 8
  • 17