Reputation: 1714
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
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:
users
state []
is logged in the second useEffect
hook.useEffect
with dependency on initialUsers
runs and updates the users
state to the value of the initialUsers
state. []
(but a different reference).useEffect
hook logs the users
state update, again []
.fetchUsers
handler has fetched data and enqueues an update to the initialUsers
state.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