Skip to content

Synchronizing State In React

Posted on:August 15, 2024

I recently came across some React code which involved editing a user. In looking at this code I realized a subtle bug that required some refactor.

Let’s take a look at some similar code.

Code

import { useEffect, useState } from "react";

type User = {
  id: number;
  name: string;
};

export default function App() {
  const [user, setUser] = useState<User>();

  useEffect(() => {
    setTimeout(() => {
      setUser({
        id: 1,
        name: "Brenley",
      });
    }, 2000);
  }, []);

  return <UserDetails user={user} />;
}

type UserDetailsProps = {
  user?: {
    id: number;
    name: string;
  };
};

function UserDetails(props: UserDetailsProps) {
  const [name, setName] = useState(props.user?.name || "");

  return (
    <form>
      <label htmlFor="name">Name: </label>
      <input
        type="text"
        id="name"
        value={name}
        onChange={e => {
          setName(e.target.value);
        }}
      />
      <button type="submit">Submit</button>
    </form>
  );
}
input box blank

Weird, our input stays blank and doesn’t update with the value Brenley. How do we fix?

Effect to synchronize state?

The first thing people reach for in this case is usually another effect. This is not desirable but let’s see how to do it anyway.

function UserDetails(props: UserDetailsProps) {
  const [name, setName] = useState(props.user?.name || "");

  useEffect(() => {
    setName(props.user?.name);
  }, [props.user?.name]);

  // ...
}
input box populated

It works but you can see how using one effect to set state leads to another etc…

Key prop to the rescue?

What about instead of using an effect we use the key prop so it remounts every time the name changes.

function App() {
  // ...

  return <UserDetails key={user?.name} user={user} />;
}

This is better but still not ideal since it will render <UserDetails> once with an undefined user and another time with the actual user.

Only render with correct user?

Can we somehow render the <UserDetails> only once with the correct user?

function App() {
  const [user, setUser] = useState<User>();

  // ...

  if (!user) {
    return "Loading...";
  }

  return <UserDetails user={user} />;
}

YES, We add a simple guard that shows a loading indicator!

Now because of this we also know that user will always be defined because we’ve guarded it in this parent component. We can remove the ? from the UserDetailsProps type and remove our || '' default values.

// before
type UserDetailsProps = {
  user?: {
    id: number;
    name: string;
  };
};

// after
type UserDetailsProps = {
  user: {
    id: number;
    name: string;
  };
};
// before
const [name, setName] = useState(props.user.name || "");

// after
const [name, setName] = useState(props.user.name);

You see how we have the best of both worlds. We have the most performant code, along with the most clear code. It’s nice when these align!