No history yet

Professional State Patterns

From Simple State to Complex UIs

So far, we've used useState to manage simple values like counters and strings. But real-world applications often involve more complex scenarios, like handling user input from forms and managing data that needs to be fetched from a server. In these cases, how you structure and manage your state is crucial for building robust and maintainable components.

Controlled Components

In standard HTML, form elements like <input>, <textarea>, and <select> typically maintain their own state and update it based on user input. These are called uncontrolled components because the DOM handles the data.

In React, the idiomatic way is to use what are known as controlled components. With this pattern, the React component's state is the "single source of truth" for the input element's value. Every state mutation has an associated handler function, making it easy to modify or validate user input.

function NameForm() {
  const [name, setName] = React.useState('');

  const handleChange = (event) => {
    setName(event.target.value.toUpperCase()); // Example: force uppercase
  };

  return (
    <form>
      <label>
        Name:
        <input type="text" value={name} onChange={handleChange} />
      </label>
    </form>
  );
}

In this example, the value of the input is always driven by the name state variable. When the user types, the onChange event fires, calling handleChange, which updates the state. This update triggers a re-render, and the input displays the new value from the state. The React component controls the input's value.

Managing Form State

What about forms with multiple inputs? You could use a separate useState call for each field, but it's often cleaner to manage the form's state as a single object. This keeps related data grouped together.

function SignUpForm() {
  const [formState, setFormState] = React.useState({
    firstName: '',
    email: '',
  });

  const handleChange = (event) => {
    const { name, value } = event.target;
    setFormState(prevState => ({
      ...prevState, // Copy the old fields
      [name]: value   // Overwrite the changed field
    }));
  };

  return (
    <form>
      <input
        name="firstName"
        value={formState.firstName}
        onChange={handleChange}
      />
      <input
        type="email"
        name="email"
        value={formState.email}
        onChange={handleChange}
      />
    </form>
  );
}

Notice how the name attribute of each input matches a key in our state object. This allows us to use a single handleChange function for all inputs. We use the spread syntax (...prevState) to create a new object, ensuring we don't directly mutate state, and then update the specific property that changed using computed property names ([name]).

Handling Asynchronous Operations

A common use case for state is tracking the status of an asynchronous operation, like fetching data from an API. You're typically dealing with at least three states: loading, success (data received), and error. We can manage this with a single state variable.

Let’s imagine we're fetching user data. The component will exist in one of several states: idle (nothing has happened yet), loading (we're waiting for data), success (we got the data), or error (something went wrong).

function UserProfile() {
  const [status, setStatus] = React.useState('idle');
  const [user, setUser] = React.useState(null);
  const [error, setError] = React.useState(null);

  const fetchUser = () => {
    setStatus('loading');
    // Faking an API call
    fakeApiCall()
      .then(userData => {
        setUser(userData);
        setStatus('success');
      })
      .catch(error => {
        setError(error);
        setStatus('error');
      });
  };

  if (status === 'loading') {
    return <p>Loading...</p>;
  }

  if (status === 'error') {
    return <p>Error: {error.message}</p>;
  }

  if (status === 'success') {
    return <h1>Welcome, {user.name}</h1>;
  }

  return <button onClick={fetchUser}>Load User</button>;
}

This pattern makes your component’s rendering logic very clear. By checking the status variable, you can display a different UI for each stage of the data fetching process. This declarative approach is much cleaner than manually showing and hiding different elements.

Knowing When to Escalate

As your component's logic grows, you might find that you have many useState calls, and the logic to update them becomes tangled. For example, if updating one piece of state always requires updating another, you might have what's called an impossible state if you forget to update both.

This is where React's useReducer hook comes in. While useState is technically built using useReducer, it's helpful to think of useReducer as a more powerful tool for complex state.

State management is one of the foundational pillars of building robust and scalable React applications.

Deciding between useState and useReducer is a common architectural choice. There's no single right answer, but here's a general guide:

FeatureuseState is a good choice when:useReducer is a better choice when:
State ShapeYou have simple state, like a number, string, or boolean.You have complex state, like a nested object or an array of objects.
State TransitionsYou have few, simple state updates.You have many state transitions, and the next state depends on the previous one.
Related StateYou're managing a few independent pieces of state.You have multiple state values that tend to change together.
Logic LocationUpdate logic is simple enough to live inside event handlers.You want to centralize and decouple the update logic from your component.
TestabilitySimple logic doesn't require complex testing.You want to easily unit test the state transition logic in isolation.

Think of useState as your default tool. It's simple and direct. When you find your state logic becoming hard to manage, with multiple setState calls in one event handler, that's a signal to consider upgrading to useReducer. This pattern helps create more predictable and maintainable components in large applications.

Quiz Questions 1/5

In React, what makes an HTML form element like <input> a "controlled component"?

Quiz Questions 2/5

When managing a form with multiple inputs using a single state object, a common pattern in the handler function is setState(prevState => ({ ...prevState, [name]: value })). What is the purpose of the [name] syntax?