226

Is there an easy way to determine which variable in a useEffect's dependency array triggers a function re-fire?

Simply logging out each variable can be misleading, if a is a function and b is an object they may appear the same when logged but actually be different and causing useEffect fires.

For example:

React.useEffect(() => {
  // which variable triggered this re-fire?
  console.log('---useEffect---')
}, [a, b, c, d])

My current method has been removing dependency variables one by one until I notice the behavior that causes excessive useEffect calls, but there must be a better way to narrow this down.

chazsolo
  • 7,873
  • 1
  • 20
  • 44
Cumulo Nimbus
  • 8,785
  • 9
  • 47
  • 68
  • 3
    Just a thought, if you need to verify which variable changed, wouldn't it make sense to have multiple `useEffects` (one for each changing variable that may change independently). Because it's clear you're trying to couple two use cases into one? – Archmede Nov 26 '21 at 18:55
  • @Archmede That sounds very repetitive if the actions needed all basically go together. – Akaisteph7 May 10 '23 at 15:23

10 Answers10

216

I ended up taking a little bit from various answers to make my own hook for this. I wanted the ability to just drop something in place of useEffect for quickly debugging what dependency was triggering useEffect.

const usePrevious = (value, initialValue) => {
  const ref = useRef(initialValue);
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};
const useEffectDebugger = (effectHook, dependencies, dependencyNames = []) => {
  const previousDeps = usePrevious(dependencies, []);

  const changedDeps = dependencies.reduce((accum, dependency, index) => {
    if (dependency !== previousDeps[index]) {
      const keyName = dependencyNames[index] || index;
      return {
        ...accum,
        [keyName]: {
          before: previousDeps[index],
          after: dependency
        }
      };
    }

    return accum;
  }, {});

  if (Object.keys(changedDeps).length) {
    console.log('[use-effect-debugger] ', changedDeps);
  }

  useEffect(effectHook, dependencies);
};

Below are two examples. For each example, I assume that dep2 changes from 'foo' to 'bar'. Example 1 shows the output without passing dependencyNames and Example 2 shows an example with dependencyNames.

Example 1

Before:

useEffect(() => {
  // useEffect code here... 
}, [dep1, dep2])

After:

useEffectDebugger(() => {
  // useEffect code here... 
}, [dep1, dep2])

Console output:

{
  1: {
    before: 'foo',
    after: 'bar'
  }
}

The object key '1' represents the index of the dependency that changed. Here, dep2 changed as it is the 2nd item in the dependency, or index 1.

Example 2

Before:

useEffect(() => {
  // useEffect code here... 
}, [dep1, dep2])

After:

useEffectDebugger(() => {
  // useEffect code here... 
}, [dep1, dep2], ['dep1', 'dep2'])

Console output:

{
  dep2: {
    before: 'foo',
    after: 'bar'
  }
}
Bradley
  • 2,379
  • 1
  • 11
  • 17
  • 7
    You may get a warning from React, saying *"React Hook useEffect has a missing dependency: 'effectHook'."* You can handle this by including the effectHook function as a dependency by simply changing `useEffect(effectHook, dependencies);` to `useEffect(effectHook, [effectHook, ...dependencies]);` – Josh Kautz Jul 06 '22 at 22:09
  • 1
    If you get an error `TypeError: Cannot read properties of undefined (reading '0')`, replace all instances of `previousDeps` with `previousDeps?.` (this is [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining)). – warren wiser Jul 22 '22 at 21:44
  • if you get: `No overload matches this call. Overload 1 of 2, '(o: {}): string[]', gave the following error. Argument of type 'unknown' is not assignable to parameter of type '{}'. Overload 2 of 2, '(o: object): string[]', gave the following error. Argument of type 'unknown' is not assignable to parameter of type 'object'.`, change `Object.keys(changedDeps` to `Object.keys(changedDeps as any[])` – Nathan Tew Dec 29 '22 at 08:36
61

@simbathesailor/use-what-changed works like a charm!

  1. Install with npm/yarn and --dev or --no-save

  2. Add import:

    import { useWhatChanged } from '@simbathesailor/use-what-changed';
    
  3. Call it:

    // (guarantee useEffect deps are in sync with useWhatChanged)
    let deps = [a, b, c, d]
    
    useWhatChanged(deps, 'a, b, c, d');
    useEffect(() => {
      // your effect
    }, deps);
    

Creates this nice chart in the console:

image loaded from github

There are two common culprits:

  1. Some Object being pass in like this:
// Being used like:
export function App() {
  return <MyComponent fetchOptions={{
    urlThing: '/foo',
    headerThing: 'FOO-BAR'
  })
}
export const MyComponent = ({fetchOptions}) => {
  const [someData, setSomeData] = useState()
  useEffect(() => {
    window.fetch(fetchOptions).then((data) => {
      setSomeData(data)
    })

  }, [fetchOptions])

  return <div>hello {someData.firstName}</div>
}

The fix in the object case, if you can, break-out a static object outside the component render:

const fetchSomeDataOptions = {
  urlThing: '/foo',
  headerThing: 'FOO-BAR'
}
export function App() {
  return <MyComponent fetchOptions={fetchSomeDataOptions} />
}

You can also wrap in useMemo:

export function App() {
  return <MyComponent fetchOptions={
    useMemo(
      () => {
        return {
          urlThing: '/foo',
          headerThing: 'FOO-BAR',
          variableThing: hash(someTimestamp)
        }
      },
      [hash, someTimestamp]
    )
  } />
}

The same concept applies to functions to an extent, except you can end up with stale closures.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Devin Rhode
  • 23,026
  • 8
  • 58
  • 72
  • 2
    (The dot means a value did not change. Green check means it did change.) There's even a babel plugin (Seriously go star this guys project!) https://github.com/simbathesailor/use-what-changed – Devin Rhode Sep 10 '20 at 21:13
  • idk why but it doesn't log anything for me – Jamil Alisgenderov Oct 19 '20 at 11:51
  • 2
    @JamilAlisgenderov I think useWhatChanged must use console.table.. so if you are trying to test in an older browser that doesn't support console.table, you could check if console.table is defined. You could also verify a normal console.log('something changed', 'table defined?', !!console.table); inside your useEffect hook logs. Otherwise... maybe file an issue on github with your react version+browser – Devin Rhode Oct 19 '20 at 14:27
  • @JamilAlisgenderov Ever figure out what was causing use-what-changed to not log anything for you? – Devin Rhode Dec 09 '20 at 21:07
  • seems it is not supported in storybook – Vinujan.S Sep 29 '21 at 14:21
  • This lib suggestion was excellent – jsaddwater Jan 23 '23 at 09:59
17

UPDATE

After a little real-world use, I so far like the following solution which borrows some aspects of Retsam's solution:

const compareInputs = (inputKeys, oldInputs, newInputs) => {
  inputKeys.forEach(key => {
    const oldInput = oldInputs[key];
    const newInput = newInputs[key];
    if (oldInput !== newInput) {
      console.log("change detected", key, "old:", oldInput, "new:", newInput);
    }
  });
};
const useDependenciesDebugger = inputs => {
  const oldInputsRef = useRef(inputs);
  const inputValuesArray = Object.values(inputs);
  const inputKeysArray = Object.keys(inputs);
  useMemo(() => {
    const oldInputs = oldInputsRef.current;

    compareInputs(inputKeysArray, oldInputs, inputs);

    oldInputsRef.current = inputs;
  }, inputValuesArray); // eslint-disable-line react-hooks/exhaustive-deps
};

This can then be used by copying a dependency array literal and just changing it to be an object literal:

useDependenciesDebugger({ state1, state2 });

This allows the logging to know the names of the variables without any separate parameter for that purpose.

Edit useDependenciesDebugger

Ryan Cogswell
  • 75,046
  • 9
  • 218
  • 198
  • I like this answer, too. Compared to my answer, it's a *bit* more work to set up, but will give better output, since each dependency gets a name, whereas mine only says which index changed. – Retsam Mar 15 '19 at 19:25
  • You might switch from a ref holding `true` and `false` to one holding `null` and `{prevValue: value}` if you wanted to log the old value as well as the new value when it changes. – Retsam Mar 15 '19 at 19:27
8

As far as I know, there's no really easy way to do this out of the box, but you could drop in a custom hook that keeps track of its dependencies and logs which one changed:

// Same arguments as useEffect, but with an optional string for logging purposes
const useEffectDebugger = (func, inputs, prefix = "useEffect") => {
  // Using a ref to hold the inputs from the previous run (or same run for initial run
  const oldInputsRef = useRef(inputs);
  useEffect(() => {
    // Get the old inputs
    const oldInputs = oldInputsRef.current;

    // Compare the old inputs to the current inputs
    compareInputs(oldInputs, inputs, prefix)

    // Save the current inputs
    oldInputsRef.current = inputs;

    // Execute wrapped effect
    func()
  }, inputs);
};

The compareInputs bit could look something like this:

const compareInputs = (oldInputs, newInputs, prefix) => {
  // Edge-case: different array lengths
  if(oldInputs.length !== newInputs.length) {
    // Not helpful to compare item by item, so just output the whole array
    console.log(`${prefix} - Inputs have a different length`, oldInputs, newInputs)
    console.log("Old inputs:", oldInputs)
    console.log("New inputs:", newInputs)
    return;
  }

  // Compare individual items
  oldInputs.forEach((oldInput, index) => {
    const newInput = newInputs[index];
    if(oldInput !== newInput) {
      console.log(`${prefix} - The input changed in position ${index}`);
      console.log("Old value:", oldInput)
      console.log("New value:", newInput)
    }
  })
}

You could use this like this:

useEffectDebugger(() => {
  // which variable triggered this re-fire?
  console.log('---useEffect---')
}, [a, b, c, d], 'Effect Name')

And you would get output like:

Effect Name - The input changed in position 2
Old value: "Previous value"
New value: "New value"
Retsam
  • 30,909
  • 11
  • 68
  • 90
4

There’s another stack overflow thread stating you can use useRef to see a previous value.

https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state

arcanereinz
  • 175
  • 4
  • 3
1

The React beta docs suggest these steps:

  • Log your dependency array with console.log:
  const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
  console.log([todos, tab]);
  • Right-click on the arrays from different re-renders in the console and select “Store as a global variable” for both of them. It may be important not to compare two sequential ones if you are in strict mode, I'm not sure.
  • Compare each of the dependencies:
  Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
Noumenon
  • 5,099
  • 4
  • 53
  • 73
1

I think I prefer the one mentioned in the OP comment, just copy-paste a useEffect for each single dependency. Super easy to understand and no real chance of new, complicated code/library going pear-shaped.

      . // etc in big hook
      .
      return complicated(thing);
    },
    [api, error, needsSeqs, throttle]
  );

  useEffect(() => console.log('api'), [api]);
  useEffect(() => console.log('error'), [error]);
  useEffect(() => console.log('needsSeqs'), [needsSeqs]);
  useEffect(() => console.log('throttle'), [throttle]);
Ron Newcomb
  • 2,886
  • 21
  • 24
0

This question was answered with several good and working answers, but I just didn't like the DX of any of them.

so I wrote a library which logs the dependencies that changed in the easiest way to use + added a function to log a deep comparison between 2 objects, so you can know what exactly changed inside your object.

I called it: react-what-changed

The readme has all of the examples you need.

The usage is very straight forward:

npm install react-what-changed --save-dev
import { reactWhatChanged as RWC } from 'react-what-changed';

function MyComponent(props) {
  useEffect(() => {
    someLogic();
  }, RWC([somePrimitive, someArray, someObject]));
}

In this package you will also find 2 useful functions for printing deep comparison (diffs only) between objects. for example:

import { reactWhatDiff as RWD } from 'react-what-changed';

function MyComponent(props) {
  useEffect(() => {
    someLogic();
  }, [somePrimitive, someArray, someObject]);

  RWD(someArray);
}
Noam L
  • 119
  • 4
0

I expanded on @Bradley's answer so this can be used with useMemo and useCallback as well. I'm pretty sure it works, though I haven't thoroughly tested it. Note that they all return their hooks, since useMemo and useCallback need returns. I haven't seen any ill effects with returning useEffect this way. Feel free to comment if you see problems or fixes.

import React, { useEffect, useRef } from 'react'

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

const hookTypes = ['Effect', 'Memo', 'Callback'] as const
export const debuggers = hookTypes.reduce(
  (obj, hookType) => ({
    ...obj,
    [`use${hookType}Debugger`]: (
      effectHook,
      dependencies,
      dependencyNames = [],
      hookName = ''
    ) => {
      const previousDeps = usePrevious(dependencies, [])

      const changedDeps = dependencies.reduce((accum, dependency, index) => {
        if (dependency !== previousDeps[index]) {
          const keyName = dependencyNames[index] || index
          return {
            ...accum,
            [keyName]: {
              before: previousDeps[index],
              after: dependency,
            },
          }
        }

        return accum
      }, {} )

      if (Object.keys(changedDeps).length) {
        console.log(`[use-${hookType.toLowerCase()}-debugger] `, hookName, changedDeps)
      }

      // @ts-ignore
      return React[`use${hookType}`](effectHook, dependencies)
    },
  }),
  {} as Record<`use${typeof hookTypes[number]}Debugger`, (
    effectHook: Function,
    dependencies: Array<any>,
    dependencyNames?: string[],
    hookName?: string
  ) => any>
)
Jonathan Tuzman
  • 11,568
  • 18
  • 69
  • 129
0

I was having the same problem, my useEffect was running more than once. What I did to find out what was causing this was I put a log statement in the beginning, to find out how many times it was running, and then I started removing the dependancies one by one and watching how many times that console.log was running. So, when the number of log statements reduce, it will be that dependancy which you removed which was causing the execution of useEffect more than once.

useEffect(() => {
console.log('I ran'); // Log statement to keep track of how many times useEffect is running
// your stuff
}, [d1, d2, d3, d4]); //Dependancies

So, remove these dependancies one by one and keep track of the console.log. If the number reduces, that dependancy was causing the issue.