Doug
Doug

Reputation: 1714

useEffect hook called multiple times when dependency value remains the same

I'm trying to figure out why my useEffect hook keeps getting called multiple times, even when the dependency has the same value. I'm using the following code:

import React, { useEffect, useState } from 'react'
import PropTypes from 'prop-types'

import Cards from '../../../cards/Cards'
import UserCard from '../../../cards/users/Card'
import LoadingContainer from '../../../LoadingContainer'
import UsersResource from '../../../../api/resources/Users'

const Users = ({ users }) => (
  <Cards>
    {users.map((user) => (
      <UserCard user={user} key={`user-${user.id}`} />
    ))}
  </Cards>
)

const UsersPage = () => {
  const [initialLoad, setInitialLoad] = useState(true)
  const [loading, setLoading] = useState(true)
  const [initialUsers, setInitialUsers] = useState([])
  const [users, setUsers] = useState([])

  const fetchUsers = async () => {
    setLoading(true)
    const response = await UsersResource.getIndex()
    setInitialUsers(response.data)
  }

  useEffect(() => {
    fetchUsers()
  }, [])

  useEffect(() => {
    console.log('users changed:', users)
    initialLoad ? setInitialLoad(false) : setLoading(false)
  }, [users])

  useEffect(() => {
    setUsers(initialUsers)
  }, [initialUsers])

  return (
    <LoadingContainer
      loading={loading}
      hasContent={!!users.length}
    >
      <Users users={users} />
    </LoadingContainer>
  )
}

Users.propTypes = {
  users: PropTypes.arrayOf(PropTypes.shape).isRequired,
}

export default UsersPage

This is the effect that gets re-run when the value of the users dependency stays the same:

useEffect(() => {
  console.log('users changed:', users)
  initialLoad ? setInitialLoad(false) : setLoading(false)
}, [users])

Here's the output:

users changed: []
users changed: []
users changed: (10) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]

So users is obviously being recognized as changed twice, even though both times the effect is called, it returns the same value. This results in my loading state being set to false before the request finishes.

It only runs once if I change the initial state assignment of users from this...

const [users, setUsers] = useState([])

To this...

const [users, setUsers] = useState(initialUsers)

This tells me that the component must be rerendering simply because users is pointing to initialUsers in the second effect, instead of just a blank array (even though initialUsers returns a blank array as well). Can anyone explain why this happens this way? I can't seem to find any documentation describing this behavior (maybe I'm blind).

I would expect the value to be the only thing to influence an effect, but it seems like it might get triggered because the dependency is pointing to a new reference in memory. Am I off?

Upvotes: 1

Views: 2224

Answers (1)

Drew Reese
Drew Reese

Reputation: 202882

This appears to be a bit of a misunderstanding between value equality and reference equality. React uses reference equality.

The initial initialUsers and users state values are [], and on the initial render cycle there is a useEffect hook that enqueues an update to users with the current initialUsers value.

Note that initialUsers isn't not the same reference as users, so initialUsers === users evaluates false.

const initialUsers = [];
const users = [];

console.log(initialUsers === users); // false

Note also that [] === [] is also never true since they are two object references.

console.log([] === []); // false

This is roughly how the logic flows:

  1. On the initial render cycle the initial users state [] is logged in the second useEffect hook.
  2. The useEffect with dependency on initialUsers runs and updates the users state to the value of the initialUsers state. [] (but a different reference).
  3. The second useEffect hook logs the users state update, again [].
  4. The fetchUsers handler has fetched data and enqueues an update to the initialUsers state.
  5. The second useEffect hook logs the users state update, now a populated array.

Code:

const fetchUsers = async () => {
  setLoading(true);
  const response = await axios.get('https://jsonplaceholder.typicode.com/users');

  // (4) update initialUsers
  setInitialUsers(response.data);
};

useEffect(() => {
  fetchUsers();
}, []);

useEffect(() => {
  // (1) initial render, first log "[]"
  // (3) second render, second log "[]"
  // (5) third render, third log "[.........]"
  console.log("users changed:", users);
  initialLoad ? setInitialLoad(false) : setLoading(false);
}, [users]);

useEffect(() => {
  // (2) initial render update users
  setUsers(initialUsers);
}, [initialUsers]);

The difference when you initialize the users state to the initialState value is now they are the same reference.

const initialUsers = [];
const users = initialUsers;

console.log(initialUsers === users); // true

This subtle difference skips the enqueued update #2 above since users and initialUsers are already the same reference.

Upvotes: 2

Related Questions