How State Works in React – Explained with Code Examples

React has become one of the most popular JavaScript libraries for building user interfaces. A core concept in React is state – an object that represents the parts of the app that can change over time. Properly understanding and leveraging state is key to building dynamic, interactive React apps.

In this article, we‘ll take an in-depth look at how state works in React. We‘ll cover the basics of initializing and updating state, how state differs in class vs functional components, and some best practices and more advanced techniques for managing state in larger applications. Let‘s dive in!

What is State in React?

In React, state refers to an object that represents the internal data of a component that can change over the lifetime of the component. When the state object changes, the component re-renders itself, updating the UI to reflect the new state.

Here are some key things to know about state:

  • State is mutable, meaning it can be changed using the setState method in class components or the useState hook in functional components
  • Changes in state trigger React to re-render the component and potentially child components
  • State should be considered private to a component and not accessed or modified outside that component

State is what allows you to create components that are dynamic and responsive to user input. Some examples of things that would be stored in state include:

  • Data fetched from an API
  • Whether a modal is open or closed
  • The current value of a form input
  • Which navigation tab is active

Now that we have an overview of what state is, let‘s look at some examples of how to actually use it in components.

Initializing and Using State in Class Components

Prior to the introduction of hooks, class components were the main way to handle state in React. Even with hooks, it‘s still important to understand class components and how they work.

Here‘s a simple example of initializing state in a class component:

import React from ‘react‘;

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
      </div>
    );
  }
}

To initialize state in a class component:

  1. Define a constructor method that takes props as an argument
  2. Call super(props) to initialize the parent React.Component class
  3. Initialize this.state as an object with the default state values

To use the state in the render method, access it as this.state.propertyName.

Updating State in Class Components

To modify state in a class component, you need to call this.setState() and pass it an object that will be merged into the current state.

Continuing with our counter example, here‘s how we can add a button to increment the count:

import React from ‘react‘;

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  handleIncrement = () => {
    this.setState(prevState => ({
      count: prevState.count + 1
    }));
  }

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.handleIncrement}>+</button>
      </div>
    );
  }
}

A few things to note about updating state:

  • Always use setState and never try to modify this.state directly
  • setState accepts an object OR a function that returns an object
  • When the new state depends on the previous state, pass a function to setState to ensure the updates are queued and executed in the correct order

State in Functional Components with Hooks

With the introduction of hooks, you can now add state to functional components. The main hook for dealing with state is the useState hook.

Here‘s our counter example rewritten as a functional component with useState:

import React, { useState } from ‘react‘;

function Counter() {
  const [count, setCount] = useState(0);

  function handleIncrement() {
    setCount(prevCount => prevCount + 1);
  }

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>+</button>  
    </div>
  );
}

useState takes the initial state value as an argument and returns an array containing the current state value and a function for updating it.

You can add as many useState calls as you need for all the different state values in your component.

When to Use State vs Props

Another important concept in React is props – data that is passed into a component from its parent. It‘s important to understand the difference between props and state:

  • State is internal to a specific component and can only be updated by that component
  • Props are passed into a component from a parent component and are read-only; a component cannot change its own props

A general rule of thumb is that data that remains constant should be passed in as props, while data that will change over time within a component should be part of its state.

Immutable State Updates

A very important rule when working with state is to keep it immutable – meaning you should not directly modify state, but instead return new versions of the state.

For example, if your state contained an array of todo items, you would not want to directly modify that array:

// Wrong: this.state.todos.push(newTodo)

Instead, you would create a new copy of the array with the update:

// Correct:
this.setState(prevState => ({
    todos: [...prevState.todos, newTodo]
})); 

The same idea applies to objects in state – make copies of them and modify the copies rather than the originals.

This allows React to correctly determine if state has actually changed to decide if a re-render is necessary. It also opens up possibilities for cool features like undo/redo.

Lifting State Up

As your app grows in complexity, you may find that multiple components need access to the same state. In this case, it‘s best to lift the shared state up to the closest common ancestor component.

For example, let‘s say we have two sibling components that need access to a user object. Instead of duplicating the user state in each one, we could store it in their parent component and pass it down as props.

Parent component:

function App() {
  const [user, setUser] = useState({
    name: ‘John‘,
    email: ‘[email protected]‘
  });

  return (
    <>
      <Header user={user} />
      <Content user={user} />
    </>
  );
}

Header component:

function Header({ user }) {
  return ;
}

Content component:

function Content({ user }) {
  return <h2>{user.email}</h2>;
}

By lifting the state up, we keep a single source of truth and allow data to be shared across the component tree.

Managing Complex State with useReducer

For more complex state updates that involve multiple sub-values or complex logic, the useReducer hook can be very helpful.

useReducer is an alternative to useState and is based on the concept of Redux reducers. It takes a reducer function that specifies how the state gets updated and returns the current state along with a dispatch method to trigger updates.

Here‘s an example of using useReducer to manage state for a shopping cart:

import React, { useReducer } from ‘react‘;

function reducer(state, action) {
  switch (action.type) {
    case ‘add‘:
      return {
        ...state, 
        cart: [...state.cart, action.item]
      };
    case ‘remove‘:
      const index = state.cart.findIndex(i => i.id === action.id);
      return {
        ...state,
        cart: [
          ...state.cart.slice(0, index),
          ...state.cart.slice(index + 1)
        ]
      };
    default:
      return state;
  }
}

function ShoppingCart() {
  const [state, dispatch] = useReducer(reducer, { cart: [] });

  function handleAdd(item) {
    dispatch({ type: ‘add‘, item });
  }

  function handleRemove(id) {
    dispatch({ type: ‘remove‘, id });
  }

  return (
    <div>
      {state.cart.map(item => (
        <div>
          <span>{item.name}</span>
          <button onClick={() => handleRemove(item.id)}>
            Remove
          </button>
        </div>
      ))}
      <button onClick={() => handleAdd({
        id: Date.now(), 
        name: ‘New Item‘
      })}>
        Add Item
      </button>
    </div>
  );
}

The reducer function takes the current state and an action object and returns the new state based on the action type.

This allows you to centralize your state update logic and handle more complex updates in a clean way.

State Management Libraries

For large applications with complex state needs spanning many components, it can be worth considering a third-party state management library.

Some popular ones in the React ecosystem include:

  • Redux
  • MobX
  • Recoil

These libraries provide robust frameworks for storing application state, updating it in a predictable way, and connecting it with your React components.

They can be especially useful for sharing state between very distant components or keeping a normalized cache of API data.

State Management Best Practices

Here are some tips for effectively managing state in your React apps:

  • Keep your state as simple and minimal as possible. Avoid duplicating data in multiple places.
  • Lift state up to the closest common ancestor that needs it, but no further. This keeps components decoupled.
  • For complex update logic or state that depends on previous state, consider useReducer.
  • Encapsulate state in custom hooks if it needs to be reused across components.
  • Use React devtools to inspect and debug your state as your app is running.

With these guidelines in mind, you‘ll be able to keep the state in your React apps organized and maintainable.

Conclusion

State is a critical concept to understand when building React applications. By using state, you can create dynamic, data-driven UIs that respond to user input.

In this article, we covered all the fundamentals of state including:

  • Initializing and updating state in class and functional components
  • Best practices like keeping state immutable and lifting it up
  • Using useReducer for complex state updates
  • Tips for organizing state in larger apps

To dive even deeper, I recommend checking out the official React docs on state as well as experimenting with building your own stateful components hands-on. Happy coding!

Similar Posts