Choosing the State Structure

Structuring state well can make a difference between a component that is pleasant to modify and debug, and one that is a constant source of bugs. Here are some tips you should consider when structuring state.

You will learn

  • When to use a single vs multiple state variables
  • What to avoid when organizing state
  • How to fix common issues with the state structure

Principles for structuring state

When you write a component that holds some state, you’ll have to make choices about how many state variables to use and what the shape of their data should be. While it’s possible to write correct programs even with a suboptimal state structure, there are a few principles that can guide you to make better choices:

  1. Group related state. If you always update two or more state variables at the same time, consider merging them into a single state variable.
  2. Avoid contradictions in state. When the state is structured in a way that several pieces of state may contradict and “disagree” with each other, you leave room for mistakes. Try to avoid this.
  3. Avoid redundant state. If you can calculate some information from the component’s props or its existing state variables during rendering, you should not put that information into that component’s state.
  4. Avoid duplication in state. When the same data is duplicated between multiple state variables, or within nested objects, it is difficult to keep them in sync. Reduce duplication when you can.
  5. Avoid deeply nested state. Deeply hierarchical state is not very convenient to update. When possible, prefer to structure state in a flat way.

The goal behind these principles is to make state easy to update without introducing mistakes. Removing redundant and duplicate data from state helps ensure that different pieces of it don’t get out of sync. This is similar to how a database engineer might want to “normalize” the database structure to reduce the chance of bugs. To paraphrase Albert Einstein, “Make your state as simple as it can be—but no simpler.”

Now let’s see how these principles apply in action.

You might sometimes be unsure between using a single or multiple state variables.

Should you do this?

const [x, setX] = useState(0);
const [y, setY] = useState(0);

Or this?

const [position, setPosition] = useState({ x: 0, y: 0 });

Technically, you can use either of these approaches. But if some two state variables always change together, it might be a good idea to unify them into a single state variable. Then you won’t forget to always keep them in sync, like in this example where hovering updates both of the red dot’s coordinates:

import { useState } from 'react';
export default function MovingDot() {
  const [position, setPosition] = useState({
    x: 0,
    y: 0
  });
  return (
    <div
      onPointerMove={e => {
        setPosition({
          x: e.clientX,
          y: e.clientY
        });
      }}
      style={{
        position: 'relative',
        width: '100vw',
        height: '100vh',
      }}>
      <div style={{
        position: 'absolute',
        backgroundColor: 'red',
        borderRadius: '50%',
        transform: `translate(${position.x}px, ${position.y}px)`,
        left: -10,
        top: -10,
        width: 20,
        height: 20,
      }} />
    </div>
  )
}

Another case where you’ll group data into an object or an array is when you don’t know how many different pieces of state you’ll need. For example, it’s helpful when you have a form where the user can add custom fields.

Gotcha

If your state variable is an object, remember that you can’t update only one field in it without explicitly copying the other fields. For example, you can’t do setPosition({ x: 100 }) in the above example because it would not have the y property at all! Instead, if you wanted to set x alone, you would either do setPosition({ ...position, x: 100 }) or you would need to split them into two state variables, and do setX(100).

Avoid contradictions in state

Here is a form with isSending and isSent state variables:

import { useState } from 'react';

export default function Chat() {
  const [text, setText] = useState('');
  const [isSending, setIsSending] = useState(false);
  const [isSent, setIsSent] = useState(false);

  async function handleSubmit(e) {
    e.preventDefault();
    setIsSending(true);
    await sendMessage(text);
    setIsSending(false);
    setIsSent(true);
  }

  if (isSent) {
    return <h1>Sent!</h1>
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button
        disabled={isSending}
        type="submit"
      >
        Send
      </button>
      {isSending && <p>Sending...</p>}
    </form>
  );
}

// Pretend to send a message.
function sendMessage(text) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  });
}

While this code works, it leaves the door open for “impossible” states. For example, if you forget to call setIsSent and setIsSending together, you may end up in a situation where both isSending and isSent are true at the same time. The more complex your component is, the harder it will be to understand what happened.

Since isSending and isSent should never be true at the same time, it is better to replace them with one status state variable that may take one of three valid states: 'typing' (initial), 'sending', and 'sent':

import { useState } from 'react';

export default function Chat() {
  const [text, setText] = useState('');
  const [status, setStatus] = useState('typing');

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('sending');
    await sendMessage(text);
    setStatus('sent');
  }

  const isSending = status === 'sending';
  const isSent = status === 'sent';

  if (isSent) {
    return <h1>Sent!</h1>
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button
        disabled={isSending}
        type="submit"
      >
        Send
      </button>
      {isSending && <p>Sending...</p>}
    </form>
  );
}

// Pretend to send a message.
function sendMessage(text) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  });
}

You can still declare some constants for readability:

const isSending = status === 'sending';
const isSent = status === 'sent';

But they’re not state variables, so you don’t need to worry about them getting out of sync with each other.

Avoid redundant state

If you can calculate some information from the component’s props or its existing state variables during rendering, you should not put that information into that component’s state.

For example, take this form. It works, but can you find any redundant state in it?

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState('');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
    setFullName(e.target.value + ' ' + lastName);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
    setFullName(firstName + ' ' + e.target.value);
  }

  return (
    <>
      <label>
        First name:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <h3>
        Your full name is: {fullName}
      </h3>
    </>
  );
}

This form has three state variables: firstName, lastName, and fullName. However, fullName is redundant. You can always calculate fullName from firstName and lastName during render, so remove it from state.

This is how you can do it:

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  const fullName = firstName + ' ' + lastName;

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <label>
        First name:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <h3>
        Your full name is: {fullName}
      </h3>
    </>
  );
}

Here, fullName is not a state variable. Instead, it’s calculated during render:

const fullName = firstName + ' ' + lastName;

As a result, the change handlers don’t need to do anything special to update it. When you call setFirstName or setLastName, you trigger a re-render, and then the next fullName will be calculated from the fresh data.

Deep Dive

Don't mirror props in state

Avoid duplication in state

This menu list component lets you choose a single dish out of several:

import { useState } from 'react';

const initialItems = [
  { title: 'Raddish', id: 0 },
  { title: 'Celery', id: 1 },
  { title: 'Carrot', id: 2 },
]

export default function CafeMenu() {
  const [items, setItems] = useState(initialItems);
  const [selectedItem, setSelectedItem] = useState(
    items[0]
  );

  return (
    <>
      <ul>
        {items.map(item => (
          <li key={item.id}>
            {item.title}
            {' '}
            <button onClick={() => {
              setSelectedItem(item);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}</p>
    </>
  );
}

Currently, it stores the selected item as an object in the selectedItem state variable. However, this is not great: the contents of the selectedItem is the same object as one of the items inside the items list. This means that the information about the item itself is duplicated in two places.

Why is this a problem? Let’s make each item editable:

import { useState } from 'react';

const initialItems = [
  { title: 'Raddish', id: 0 },
  { title: 'Celery', id: 1 },
  { title: 'Carrot', id: 2 },
]

export default function CafeMenu() {
  const [items, setItems] = useState(initialItems);
  const [selectedItem, setSelectedItem] = useState(
    items[0]
  );

  function handleItemChange(id, e) {
    setItems(items.map(item => {
      if (item.id === id) {
        return {
          ...item,
          title: e.target.value,
        };
      } else {
        return item;
      }
    }));
  }

  return (
    <>
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={e => {
                handleItemChange(item.id, e)
              }}
            />
            {' '}
            <button onClick={() => {
              setSelectedItem(item);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}</p>
    </>
  );
}

Notice how if you first click “Choose” on an item and then edit it, the input updates but the label at the bottom does not reflect the edits. This is because you have duplicated state, and you forgot to update selectedItem.

Although you could update selectedItem too, an easier fix is to remove duplication. In this example, instead of a selectedItem object (which creates a duplication with objects inside items), you hold the selectedId in state, and then get the selectedItem by searching the items array for an item with that ID:

import { useState } from 'react';

const initialItems = [
  { title: 'Raddish', id: 0 },
  { title: 'Celery', id: 1 },
  { title: 'Carrot', id: 2 },
]

export default function CafeMenu() {
  const [items, setItems] = useState(initialItems);
  const [selectedId, setSelectedId] = useState(0);

  const selectedItem = items.find(item =>
    item.id === selectedId
  );

  function handleItemChange(id, e) {
    setItems(items.map(item => {
      if (item.id === id) {
        return {
          ...item,
          title: e.target.value,
        };
      } else {
        return item;
      }
    }));
  }

  return (
    <>
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={e => {
                handleItemChange(item.id, e)
              }}
            />
            {' '}
            <button onClick={() => {
              setSelectedId(item.id);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}</p>
    </>
  );
}

(Alternatively, you may hold the selected index in state.)

The state used to be duplicated like this:

  • items = [{ id: 0, text: 'Raddish'}, ...]
  • selectedItem = {id: 0, text: 'Raddish}

But after the change it’s like this:

  • items = [{ id: 0, text: 'Raddish'}, ...]
  • selectedId = 0

The duplication is gone, and you only keep the essential state!

Now if you edit the selected item, the message below will update immediately. This is because setItems triggers a re-render, and items.find(...) would find the item with the updated text. You didn’t need to hold the selected item in state, because only the selected ID is essential. The rest could be calculated during render.

Avoid deeply nested state

Imagine a todo list where tasks can be arbitrarily nested. You might be tempted to structure its state using nested objects and arrays, like in this example:

import { useState } from 'react';

const initialRootTask = {
  id: 1,
  text: 'Root task',
  childTasks: [{
    id: 2,
    text: 'First subtask',
    childTasks: [{
      id: 3,
      text: 'First subtask of first subtask',
      childTasks: []
    }]
  }, {
    id: 4,
    text: 'Second subtask',
    childTasks: [{
      id: 5,
      text: 'First subtask of second subtask',
      childTasks: [],
    }, {
      id: 6,
      text: 'Second subtask of second subtask',
      childTasks: [],
    }]
  }]
};

function Task({ task }) {
  const childTasks = task.childTasks;
  return (
    <>
      <li>{task.text}</li>
      {childTasks.length > 0 && (
        <ol>
          {childTasks.map(task => (
            <Task key={task.id} task={task} />
          ))}
        </ol>
      )}
    </>
  );
}

export default function TaskManager() {
  const [root, setRoot] = useState(initialRootTask);
  return <ol><Task task={root} /></ol>;
}

Now let’s say you want to add a button to delete a task. How would you go about it? Updating nested state involves making copies of objects all the way up from the part that changed. For example, deleting a deeply nested task would involve copying all its entire parent task chain. For deeply nested state, this can be very cumbersome.

If the state is too nested to update easily, consider making it “flat”. Here is one way you can restructure this data. Instead of a tree-like structure where each task has an array of its child tasks, you can have each task hold an array of its child task IDs. Then you can store a mapping from each task ID to the corresponding task.

This restructuring of the data might remind you of seeing a database table:

import { useState } from 'react';

const initialTasksById = {
  1: {
    id: 1,
    text: 'Root task',
    childIds: [2, 4]
  },
  2: {
    id: 2,
    text: 'First subtask',
    childIds: [3]
  }, 
  3: {
    id: 3,
    text: 'First subtask of first subtask',
    childIds: []
  },
  4: {
    id: 4,
    text: 'Second subtask',
    childIds: [5, 6],   
  },
  5: {
    id: 5,
    text: 'First subtask of second subtask',
    childIds: [],   
  },
  6: {
    id: 6,
    text: 'Second subtask of second subtask',
    childIds: [],   
  },
};

function Task({ id, tasksById }) {
  const task = tasksById[id];
  const childIds = task.childIds;
  return (
    <>
      <li>{task.text}</li>
      {childIds.length > 0 && (
        <ol>
          {childIds.map(childId => (
            <Task
              key={childId}
              id={childId}
              tasksById={tasksById}
            />
          ))}
        </ol>
      )}
    </>
  );
}

export default function TaskManager() {
  const [
    tasksById,
    setTasksById
  ] = useState(initialTasksById);
  return (
    <ol>
      <Task
        id={1}
        tasksById={tasksById}
      />
    </ol>
  );
}

Now that the state is “flat” (also known as “normalized”), updating nested items becomes easier.

In order to remove a task now, you only need to update two levels of state:

  • The next version of its parent task should not have the deleted child’s ID in its childIds array.
  • The next version of the root tasksById object should include the new version of the parent task.

Here is an example of how you could go about it:

import { useState } from 'react';

const initialTasksById = {
  1: {
    id: 1,
    text: 'Root task',
    childIds: [2, 4]
  },
  2: {
    id: 2,
    text: 'First subtask',
    childIds: [3]
  }, 
  3: {
    id: 3,
    text: 'First subtask of first subtask',
    childIds: []
  },
  4: {
    id: 4,
    text: 'Second subtask',
    childIds: [5, 6],   
  },
  5: {
    id: 5,
    text: 'First subtask of second subtask',
    childIds: [],   
  },
  6: {
    id: 6,
    text: 'Second subtask of second subtask',
    childIds: [],   
  },
};

export default function TaskManager() {
  const [
    tasksById,
    setTasksById
  ] = useState(initialTasksById);

  function handleRemove(parentId, childId) {
    const parent = tasksById[parentId];
    // Create a new version of the parent task
    // that doesn't include this child ID.
    const nextParent = {
      ...parent,
      childIds: parent.childIds
        .filter(id => id !== childId)
    }
    // Update the root state object...
    setTasksById({
      ...tasksById,
      // ...so that it has the updated parent.
      [parentId]: nextParent,
    });
  }

  return (
    <ol>
      <Task
        id={1}
        parentId={0}
        tasksById={tasksById}
        onRemove={handleRemove}
      />
    </ol>
  );
}

function Task({ id, parentId, tasksById, onRemove }) {
  const task = tasksById[id];
  const childIds = task.childIds;
  return (
    <>
      <li>
        {task.text}
        {parentId !== 0 &&
          <button onClick={() => {
            onRemove(parentId, id);
          }}>
            Remove
          </button>
        }
      </li>
      {childIds.length > 0 &&
        <ol>
          {childIds.map(childId => (
            <Task
              key={childId}
              id={childId}
              parentId={id}
              tasksById={tasksById}
              onRemove={onRemove}
            />
          ))}
        </ol>
      }
    </>
  );
}

You can nest state as much as you like, but making it “flat” can solve numerous problems. It makes state easier to update, and it helps ensure you don’t have duplication in different parts of a nested object.

Deep Dive

Improving memory usage

Sometimes, you can also reduce state nesting by moving some of the nested state into the child components. This works well for ephemeral UI state that doesn’t need to be stored, like whether an item is hovered.

Recap

  • If two state variables always update together, consider merging them into one.
  • Choose your state variables carefully to avoid creating “impossible” states.
  • Structure your state in a way that reduces the chances that you’ll make a mistake updating it.
  • Avoid redundant and duplicate state so that you don’t need to keep it in sync.
  • Don’t put props into state unless you specifically want to prevent updates.
  • For UI patterns like selection, keep ID or index in state instead of the object itself.
  • If updating deeply nested state is cumbersome, try flattening it.

Challenge 1 of 4:
Fix a component that’s not updating

This Clock component receives two props: color and time. When you select a different color in the select box, the Clock component receives a different color prop from its parent component. However, for some reason, the displayed color doesn’t update. Why? Fix the problem.

import { useState } from 'react';

export default function Clock(props) {
  const [color, setColor] = useState(props.color);
  return (
    <h1 style={{ color: color }}>
      {props.time}
    </h1>
  );
}