2

codesandbox here: https://codesandbox.io/s/restless-haze-v01wv?file=/src/App.js

I have a Users component which (when simplified) looks something like this:

const Users = () => {
  const [toastOpen, setToastOpen] = useState(false)

  // functions to handle toast closing
  return (
   <EditUser />
   <Toast />
  )
}

const EditUser = () => {
  [user, setUser] = useState(null)
  useEffect(() => {
    const fetchedUser = await fetchUser()
    setUser(fetchedUser)
  }, [])

  // this approach results in UserForm's username resetting when the toast closes
  const Content = () => {
    if (user) return <UserForm user={user} />
    else return <div>Loading...</div>
  }
  return <Content />

  // if I do this instead, everything's fine
  return (
    <div>
    {
      user ? <UserForm user={user} /> : <div>Loading...</div>
    }
    </div>
  )
}

const UserForm = ({ user }) => {
  const [username, setUsername] = useState(user.name)

  return <input value={username}, onChange={e => setUsername(e.target.value)} />
}

While viewing the UserForm page while a Toast is still open, the UserForm state is reset when the Toast closes.

I've figured out that the issue is the Content component defined inside of EditUser, but I'm not quite clear on why this is an issue. I'd love a walkthrough of what's happening under React's hood here, and what happens in a "happy path"

2 Answers2

1

You have defined Content inside EditUser component which we never do with React Components, because in this situtaion, Content will be re-created every time the EditUser is re-rendered. (surely, EditUser is going to be re-rendered few/many times).

So, a re-created Content component means the old Content will be destroyed (unmounted) and the new Content will be mounted.

That's why it is be being mounted many times and hence resetting the state values to initial values.

So, the solution is to just define it (Content) outside - not inside any other react component.

Ajeet Shah
  • 18,551
  • 8
  • 57
  • 87
  • Makes sense, thanks. So when React goes to render ``, is this what happens: "I must render a Content component. The Content function is different from the last time I rendered this Component, so I must unmount the previous Content component and recreate one from scratch"? – luke.belliveau Mar 10 '21 at 21:00
  • 1
    Exactly! That's correct. And as it renders a **new** component, it sets up new state and all from scratch, and it loses all previous state values. I have not tried it, but i guess, we can **keep** a component definition inside a component and use **useCallback** hook to prevent it from getting **re-created**, but it would be doing a simple thing in a complex way, so not recommended. – Ajeet Shah Mar 10 '21 at 21:17
0

The culprit was EditUser's Content function, which predictably returns a brand new instance of each time it's called.

  • Yes. It should not be defined inside a Component. – Ajeet Shah Mar 10 '21 at 20:06
  • Could you explain why? I have a feeling it has something to do with creating a new closure (which creates a new component with new state every time), but I'm really fuzzy on the details. I'd love a quick walkthrough on what happens in this fail case, and in a "correct" case. – luke.belliveau Mar 10 '21 at 20:27