0

I want to create a form to edit a use profile. The form renders data from the user object. I want to use React's useState Hook to hold the state of the form and I want to keep a single object to track changes to the form using an onChange function that handles the changes to the whole user object. Why is this not working?

function Profile() {

  const [user, setUser] = useState({});
  const [errors, setErrors] = useState({});

  useEffect(() => {
    axios.get(`/api/v1/users/me`)
      .then(res => setUser(res.data.user))
  }, [])

  const onChange = e => {
    user[e.target.name] = e.target.value;
    setUser(user)
  }

  return (
    < div >
      <form onSubmit={null}>
        <div className="form-group">
          <label htmlFor={user.name}>Name</label>
          <input type="text" name="name"
            className={`form-control form-control-lg ${errors.name ? 'is-invalid' : ''}`}
            onChange={onChange} placeholder="Fred Flintstone" value={user.name || ''}
          />
        </div>
        <div className="form-group">
          <label htmlFor="email">Email</label>
          <input type="email" name="email"
            className={`form-control form-control-lg ${errors.email ? 'is-invalid' : ''}`}
            onChange={onChange} placeholder="fred.flintstone@aol.com" value={user.email || ''}
          />
        </div>
        <div className="form-group">
          <label htmlFor="username">Username</label>
          <input type="text" name="username"
            className={`form-control form-control-lg ${errors.username ? 'is-invalid' : ''}`}
            onChange={onChange} placeholder="yabadabadu" value={user.username || ''}
          />
        </div>
      </form>
      <div>
        <button type="button" className="btn btn-light btn-sm float-right" onClick={() => console.log("Logout")}>Logout</button>
      </div>
    </div >
  )
}
Emile Bergeron
  • 17,074
  • 5
  • 83
  • 129
brauliopf
  • 61
  • 1
  • 2
  • 11
  • `user[e.target.name] = e.target.value;` this is [mutating the current state, which is an anti-pattern](https://stackoverflow.com/q/37755997/1218980) in React. – Emile Bergeron Mar 02 '20 at 18:24

2 Answers2

4

You're modifying the user object in-place. When you call setUser(user), the form won't re-render because the identity of the user object hasn't changed.

Where you have:

  const onChange = e => {
    user[e.target.name] = e.target.value;
    setUser(user)
  }

what you want to have instead is something like:

  const onChange = useCallback((event) => {
    const {name, value} = event.target;
    setUser(oldUser => {
      return {
        ...user,
        [name]: value,
      };
    });
  }, [setUser]);

As a general rule of thumb, you usually don't want to modify state objects in-place in React.

  • 1
    I think `user` should be in the dependencies as well, or you could use the callback form `setUser(oldUser => ({/**/}))` instead. – Emile Bergeron Mar 02 '20 at 18:28
  • OOPS. I meant to write the callback form there! – Richard Barrell Mar 02 '20 at 18:31
  • Thank you Emile! – Richard Barrell Mar 02 '20 at 18:33
  • 1
    But then, you get another problem, [event pooling in React](https://stackoverflow.com/q/36114196/1218980) will mess up now that the event is used asynchronously, which [Dennis' answer](https://stackoverflow.com/a/60494570/1218980) got right. – Emile Bergeron Mar 02 '20 at 18:34
  • `setUser` always holds the same reference, no need it in dep array (you also might have lint warning for it) – Dennis Vash Mar 02 '20 at 18:34
  • Ouch... ok. I had not noticed I was doing that (changing the user). Still, I couldn't fix it. Look, I trie this: `const onChange = e => { const newUser = user; newUser[e.target.name] = e.target.value; setUser(newUser) }` – brauliopf Mar 02 '20 at 18:42
  • 1
    Nice Richard and Emilie. Thank ou a lot for your support. I got it to work with: const onChange = e => { setUser({ ...user, [e.target.name]: [e.target.value] }) } – brauliopf Mar 02 '20 at 18:50
1

You should make a copy of users or it will not trigger a render phase as React performs a shallow comparison with the previous state.

const onChange = ({ target: { name, value } }) => {
  setUser(user => ({ ...user, [name]: value }));
};

setState() will always lead to a re-render unless shouldComponentUpdate() returns false. If mutable objects are being used and conditional rendering logic cannot be implemented in shouldComponentUpdate(), calling setState() only when the new state differs from the previous state will avoid unnecessary re-renders.

Dennis Vash
  • 50,196
  • 9
  • 100
  • 118