'useEffect and dependency array

I am trying to understand how react useEffect works with objects.

Here is an example that will help demonstrate my question:

export function UseEffectHook() {
  const initialState = {
    firstName: "Joe",
    lastName: "Smith",
    address: {
      home: "123 street"
    }
  };
  const [user, setUser] = useState(initialState);
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log("triggered");
  }, [user]);

  return (
    <div>
      <p>useEffect - Practice with different deps</p>
      {JSON.stringify(user)}
      <label>Firstname:</label>

      <input
        type="text"
        onChange={e => setUser({ ...user, firstName: e.target.value })}
      />
      <br />
      <label>Lastname:</label>
      <input
        type="text"
        onChange={e => setUser({ ...user, lastName: e.target.value })}
      />
      <button onClick={() => setCount(count + 1)}>Button</button>
   </div>
  );
}

I have read that placing an object as a dependency in the useEffect hook does not work because of how objects are compared between rerenders.

My initial thought was in this example clicking the button and updating count should also cause the useEffect hook to be updated. This is not the case and useEffect does not re-run.

I am not able to find a contrived enough example to help demonstrate when and why useEffect re-runs using objects. Any help would be greatly appreciated.



Solution 1:[1]

useEffect will re-run with an object in it's dependency array whenever that object changes references.

For example in your code, user is always stable until you call setUser in which case it will change to a new reference and run at that point.

If you instead defined user as a new object in each render, then the useEffect would instead run every time the component re-rendered.

  const user = {
    firstName: 'Joe',
    lastName: 'Smith',
    address: {
      home: '123 street',
    },
  };

It's all about referential equality. Whether previousUser===newUser on each render. React essentially performs a check very similar to that (I believe Object.is) for every variable in the dependency array.

Solution 2:[2]

Bellow your component.

changes:

  • move initialState outside UseEffectHook. You don't need to create initialState every re-render;
  • use useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed;
  • setUser and setCount with updater function;
  • handleOnChangeUser able to use for all fields of user object. Every time handleOnChangeUser - create new ref to user object. in this case useEffect able to detect changes;

import React, { useCallback, useEffect, useState } from 'react';

const initialState = {
  firstName: "Joe",
  lastName: "Smith",
  address: {
    home: "123 street",
  }
};

export function UseEffectHook() {
  const [user, setUser] = useState(initialState);
  const [count, setCount] = useState(0);
  const handleOnChangeUser = useCallback((event) => {
    const { name, value } = event;
    
    setUser(prevState => ({ ...prevState, [name]: value }));
  }, []);
  
  const increaseCount = useCallback(() => {
    setCount(prevState => prevState + 1);
  }, []);

  useEffect(() => {
    console.log("triggered");
  }, [user]);

  return (
    <div>
      <p>useEffect - Practice with different deps</p>
      {JSON.stringify(user)}

      <label>
        <span>Firstname:</span>

        <input
          type="text"
          name="firstName"
          onChange={handleOnChangeUser}
        />
      </label>
      
      <br />
      
      <label>
        <span>Lastname:</span>
        
        <input
          type="text"
          name="lastName"
          onChange={handleOnChangeUser}
        />
      </label>

      <button onClick={increaseCount}>Increase</button>
   </div>
  );
}

Solution 3:[3]

According to your example, it will not call useEffect() whenever you click the button. Otherwise, useEffect() will be invoked whenever you change the user's name by input field. (it means, useEffect() will be called by changing user value)

useEffect(() => {
  console.log("invoke");
}, [user]);

In order to invoke useEffect() whenever you click button, it should be like this.

useEffect(() => {
  console.log("invoke");
}, [count]);

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Zachary Haber
Solution 2 Kirill Skomarovskiy
Solution 3 DevProgrammer