Setting State without a Default Value

We'll start from this base. You can also use projects/character-card or start with examples/08-character-card in the project repository.

The previous example works, but you might not always have a default value for a given piece of state at the time that the component is initiatlized. For example, here is some psuedo-code:

const [character, setCharacter] = React.useState(null);

Eventually, user should be something and we'll likely get it from the API, but—initially—we don't have a value just yet. There is nothing for TypeScript to infer.

Let's assume our User model has the following shape to it:

export type CharacterType = {
  name: string;
  alignment: string;
  intelligence: number;
  strength: number;
  speed: number;
  durability: number;
  power: number;
  combat: number;
  total: number;
};

Let's also imagine for a moment that we have an asynchronous API request called getCurrentUser. We'll polyfill it as follows for now.

// Fake API Request
export const fetchCharacter = (): Promise<CharacterType> => {
  const [character] = shuffle(data);
  return Promise.resolve(character);
};

Basically, this fake API request just waits a moment and then returns an object that adheres to the User interface that we defined earlier.

We can then give TypeScript a hint to the properties that we expect the state to have once it's loaded.

const [character, setCharacter] = React.useState<CharacterType | null>(null);

We know that this component needs just the name property and that it expects it to be a string, but that's not really DRY code. Instead, we can just tell the component to expect an object that conforms to the User interface.

Our component, might look something like this.

const Application = () => {
  const [character, setCharacter] = React.useState<CharacterType | null>(null);

  React.useEffect(() => {
    fetchCharacter().then((c) => {
      setCharacter(c);
    });
  }, []);

  return (
    <main>
      {character ? <CharacterInformation character={character} /> : <Loading />}
    </main>
  );
};

What if we wanted to have a state that kept track of whether or not we loaded the component. It might look something like this.

const Application = () => {
  const [character, setCharacter] = React.useState<CharacterType | null>(null);
  const [loading, setLoading] = React.useState(true);

  React.useEffect(() => {
    fetchCharacter().then((c) => {
      setCharacter(c);
      setLoading(false);
    });
  }, []);

  return (
    <main>
      {loading ? <Loading /> : <CharacterInformation character={character} />}
    </main>
  );
};

But wait! TypeScript is angry with us. That's because it doesn't know whether or not character is null or not.

There are a few ways that we can handle this.

{
  loading ? (
    <Loading />
  ) : (
    character && <CharacterInformation character={character} />
  );
}

Or we can just flip the logic back to make sure we confirm that there is a character, this kind of defeats the purpose of having loading at all in this situation.

What's also interesting is that this doesn't satisfy TypeScript.

{
  loading && !character ? (
    <Loading />
  ) : (
    <CharacterInformation character={character} />
  );
}

Simply asserting that it doesn't exist is not enough to prove to TypeScript that it does.

Another option is to treat them independently.

<main>
  {loading && <Loading />}
  {character && <CharacterInformation character={character} />}
</main>

You can see the completed example here.

Where Are We Now?

You can find the current state of the code in:

  • examples/09-character-card-eventual-state
  • In projects/character-card on the character-card-eventual-state branch.
  • CodeSandbox.