DEV Community 👩‍💻👨‍💻

Stephane Rangaya
Stephane Rangaya

Posted on

How to make sure useEffect catches array changes

There is a way to use useEffect to only run when variables provided in a second argument are updated, like this:

const [count, setCount] = useState(0);

useEffect(() => {
  console.log('Something happened')
}, [count]); // Only re-run the effect if count changes
Enter fullscreen mode Exit fullscreen mode

Unfortunately, this doesn't always work with arrays:

const [data, setData] = useState([
    {id: 1, name: 'Alexander'},
    {id: 2, name: 'Fabien'},
    {id: 3, name: 'Yuki'},
  ]);


useEffect(() => {
  console.log('Something happened')
}, [data]); // Changes won't be caught :( !

const onCapitalizeClick = () => {
  setData(capitalizeAllRows());
};

// in another hook:
function capitalizeAllRows() {
    setDataRows(props);
    dataRows.forEach(row => {
      row.name = Capitalize(row.name);
      setDataRows([...data]);
    });
    return dataRows;
  }
Enter fullscreen mode Exit fullscreen mode

In this case, the effect won't run again because it is comparing the array at the highest level and it is therefore not picking up the change in capitalization of names: the array has the same amount of rows, nothing structurally changed.

But if you were to add a row, the changes will be picked up and the effect would be executed.

In order to have the effect always run when any changes happen to the array, stringifying the array works well!

Instead of using data in the second argument array of useEffect(), use [JSON.stringify(data)]:

const [data, setData] = useState([
    {id: 1, name: 'Alexander'},
    {id: 2, name: 'Fabien'},
    {id: 3, name: 'Yuki'},
  ]);


useEffect(() => {
  console.log('Something happened')
}, [JSON.stringify(data)]); // Changes will be caught :) !

const onCapitalizeClick = () => {
  setData(capitalizeAllRows());
};

// in another hook:
function capitalizeAllRows() {
    setDataRows(props);
    dataRows.forEach(row => {
      row.name = Capitalize(row.name);
      setDataRows([...data]);
    });
    return dataRows;
  }
Enter fullscreen mode Exit fullscreen mode

Top comments (14)

Collapse
 
iquirino profile image
Igor Quirino

JSON.stringify(data) will add extra processing...
Why not to just add a counter (setState(0)) and then increment it when object get changed?

Collapse
 
stephane profile image
Stephane Rangaya Author

How would you do this?

Collapse
 
nicklaswinger profile image
Nicklas Pouey-Winger • Edited on

I assume by calling setLoadCount((prevState) => prevState + 1) or something like that :)

Thread Thread
 
iquirino profile image
Igor Quirino
Collapse
 
neehachoudhary profile image
NeehaChoudhary

Created an account to actually thank you for this! :D

Collapse
 
tobiasjacob profile image
Tobias Jacob

I don't think this is a good example. The fault is not, that react cannot look for array changes, but instead that you mutated your state.

NEVER MUTATE THE STATE

In the example above, this means, that you have to create a whole new array with whole new objects for react to pick up the changes. For example

const [data, setData] = useState([])

In another hook:
function capitalizeAllRows() {
setData(data.map((row) => ({...row, name: Capitalize(row.name)})))
}

note that with the spread operator, we created a new object.

I still like the idea of using hashs (for example json.stringify, even tough there are more performant ways) for applying effects if an array changes. Just the example is not well chosen

Collapse
 
mvozaar profile image
mvozaar

example is good but not for solving the problem. but for detection and verification of root cause is good :). it saves your time if you want to find problem. right solution for state changes is as you mentioned "NEVER MUTATE THE STATE".

Collapse
 
eaardal profile image
Eirik Årdal • Edited on

I ended up with wrapping the data in another object with a key field I can update on each change and listen to changes on:

// Key.ts
interface Key<T> { key: string, data: T }

// hookUtils.ts
import { Key } from '@app/models'
import { v4 as uuid } from 'uuid'

export function genId(): string {
  return uuid()
}

export function newKey<TData>(data: TData): Key<TData> {
  return { key: genId(), data }
}

// SomeComponent.tsx
import { newKey } from '@app/utils/hookUtils'

const [foos, setFoos] = useState<Key<Foo[]>>(newKey([]))

...

useEffect(() => {
    // react to foos having changed
}, [foos.key])

...

// update list somewhere
const updatedFoos = [ ...foos, { whatever: "something" } ]
setFoos(newKey(updatedFoos))

return <Foos items={foos.data} />
Enter fullscreen mode Exit fullscreen mode
Collapse
 
drberg profile image
Magnus Ringkjøb

Thanks! Your suggestion nudged me in the right direction on how to solve this. As other people has pointed out JSON.stringify can add a lot of unnecessary processing. Perhaps there's some other state you can listen to? In my case I realized the data is only updated after the loading state changed.

So for me, listening to the loading state helped me use the array of objects in useEffect as intended.

Collapse
 
harshan89 profile image
Harshan Morawaka

just use data.length as the dependency

Collapse
 
martin2844 profile image
martin2844

Thanks man. Could'nt figure out why react wasnt catching changes in my array

Collapse
 
mvozaar profile image
mvozaar

I have spotted similar behavior, when i missed object changes. changes was done by delete_by_key. this is "mutating the state".

in original code was used spread operator to clone new object, but in removal was used delete. additions was detected, but removal not.

Collapse
 
eraz7 profile image
Erfan Azary

Of course the worst solution for Object/Array comparison is JSON.stringify
especially for big Objects/Arrays, it will have massive impact on performance

Collapse
 
adsazad profile image
Arashdeep Singh Azad

didn't work.

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.