Why "setUsers([savedUser, ...users])" not make "name: Mosh" twice instead of one only?

Hello,

It’s About the video in “React 18 for beginners - Connecting to the Backend- Creating Data”

see the “red rectangle boxes”:

When we clicked “Add button” ONCE,
why this code “setUsers([savedUser, …users]))” NOT MAKING “Mosh” appears TWICE but only ONCE?

noted:
I assume/believe that in Users data, Mosh would appear twice, because of this 2 lines of code:

  • setUsers([newUser, …users])
  • setUsers([savedUser, …users]))

users:

  [
    { id: 11, name: 'Mosh' },
    { id: 0, name: 'Mosh' },
    { id: 1, name: 'Leanne Graham'},
    ...
  ]

Thank you for your help /\

Hi, I found this…

Hi @Jorge

I think it doesn’t matter whether “…reflecting a change immediately” or not.
What matters at the end, the users array is like below, consist only one name: Mosh, even though we call setUsers() TWICE

[
    **{ id: 11, name: 'Mosh' },**
    { id: 1, name: 'Leanne Graham'},
    ...
]

I put some console.log() in the code and the output:

Note: The source code is only one, in App.tsx, no others. So just copy all the code in App.tsx and npm install axios then the App can run properly well on the browser.

import axios, { CanceledError } from 'axios'
import { useEffect, useState } from 'react'

interface User {
  id: number
  name: string
}

function App() {
  const [users, setUsers] = useState<User[]>([])
  const [error, setError] = useState('')
  const [loading, setLoading] = useState(false)

  const deleteUser = (user: User) => {
    const originalUsers = [...users]
    setUsers(users.filter((u) => u.id !== user.id))

    axios
      .delete(`https://jsonplaceholder.typicode.com/users/${user.id}`)
      .catch((err) => {
        setError(err.message)
        setUsers(originalUsers)
      })
  }

  const addUser = () => {
    const originalUser = [...users]
    const newUser = { id: 0, name: 'Mosh' }
    setUsers([newUser, ...users])
    console.log('after calling 1st setUsers(), users =', users)

    axios
      .post('https://jsonplaceholder.typicode.com/users', newUser)
      .then(({ data: savedUser }) => {
        console.log('before calling 2nd setUsers(), users =', users)
        setUsers([savedUser, ...users])
        console.log('after calling 2nd setUsers(), users =', users)
      })
      .catch((err) => {
        setError(err.message)
        setUsers(originalUser)
      })
  }

  useEffect(() => {
    const controller = new AbortController()

    setLoading(true)
    axios
      .get<User[]>('https://jsonplaceholder.typicode.com/users', {
        signal: controller.signal,
      })
      .then((res) => {
        setUsers(res.data)
        setLoading(false)
      })
      .catch((err) => {
        if (err instanceof CanceledError) return
        setError(err.message)
        setLoading(false)
      })

    return () => controller.abort()
  }, [])

  console.log('users =', users)

  return (
    <>
      {error && <p className="text-danger">{error}</p>}
      {loading && <div className="spinner-border"></div>}
      <button className="btn btn-primary mb-3" onClick={addUser}>
        Add
      </button>
      <ul className="list-group">
        {users.map((user) => (
          <li
            key={user.id}
            className="list-group-item d-flex justify-content-between"
          >
            {user.name}
            <button
              className="btn btn-outline-danger"
              onClick={() => deleteUser(user)}
            >
              Delete
            </button>
          </li>
        ))}
      </ul>
    </>
  )
}

export default App

here’s the output of console.log() after clicking “Add Button” ONCE :

  • you see that the users data “after calling 1st setUsers()”, total = 11. means that “name: Mosh” is successfully added to users array and users array has 11 names.
  • see that the users data “before calling 2nd setUsers()”, users = 10. how come ? it supposed to be 11 right ?

Thank you very much for your help in teaching me /\

Hi, from what I know so far…at the second call ‘users’ has the same old 10 items…because the render is in progress yet, I found this in official react docs, check it.

Hi @Jorge

noted.
but please notice :

  • code: “console.log(‘users =’, users)”,
    where I console log the users data.
  • and the console.log output “users= (11)”

the “users data= (11)”, is after calling 1st setUsers() and before calling 2nd setUsers()

  • because “users = (11)” , it means successfully updated right and users data is 11,
    but why when calling 2nd setUsers(), the users data = 10 ?

note:
console.log(‘users =’, users) → is outside the addUser()

thanks again @Jorge

I believe the difference is because the value of users in the addUser function is a copy of the actual value (which only changes when the function gets redeclared in the next render).

I’d suggest that you not worry about this thing for now, maybe you should note it and try exploring it once you’ve completed the course. You will need to understand when and how re-rending happens in React.

Last, but not the least, this is an excellent question. I’ve been working in React JS/Native for 2 years but never saw it this way. I guess I now know why we need to put the dependency array when using the useCallback() hook.

Note to self:

const [username, setUsername] = useState("");
const myCallback = useCallback(() => {
  // username will always have the latest value
  // because it's declared in the dependency array
}, [username])

Normally in such a situation we can update the state like this:

setUsers(prev => [savedUser, ...prev])

Relevant docs section: Updating state based on the previous state

I had this same question and made a post asking about it. Someone mentioned a closure and from what I was able to research on my own it sounds like a few things are combining to create this behavior: stale closures and the fact that React bundles state updates.

What I think is happening is the first call to setUsers works fine, however since React bundles state updates and doesn’t make the change right away the users list isn’t updated immediately. Meanwhile, when we make the network call using axios, we are creating a closure, which is an inner function that has references to the variables it needs that exist outside of it. The then function of our axios call forms a closure and since it is calling setState and referencing the users list, it maintains a (separate) reference to that list for itself to use. This list doesn’t include ‘Mosh’ because React hasn’t run the state updates from the first call to setState yet.

As the code continues, React gets around to calling the first setState and it updates the users list by adding ‘Mosh’ to it. However, when the network call finishes and goes to the then function, the call to setUsers is referencing a (stale) version of the users list which does not include the ‘Mosh’ user.

So the UI updates and shows ‘Mosh’ right away because the first setState happens first whenever React gets around to calling it. But when the network call returns and adds ‘Mosh’ to the list, the user list that the closure is referencing is the one from before the first call to setState, so it’s as if the first one never happened, which is why you only see one ‘Mosh’ user.

I’m not sure if @Mosh purposely intended to use React in this way. Seems a bit funky to be using side effects of stale closures and React bundling state updates, but it works. I think it would have helped to explain this a bit more in the videos but closures are a bit more of an advanced topic I think.