No history yet

State and Lifecycle

Giving Components a Memory

So far, we've seen how props let us pass data down from a parent to a child component. This is great for creating reusable components, but it's a one-way street. What happens when a component needs to keep track of its own data that changes over time, like user input in a form or the current time on a clock? For this, components need their own internal memory. In React, this memory is called state.

Unlike props, which are read-only and controlled by a parent component, state is fully private and controlled by the component itself. It’s a place to store data that can change and will affect what the component renders.

Adding State to a Class

To add state to a class component, you initialize it inside the component's constructor. The state should always be an object. Let's make a simple clock component that needs to keep track of the current time.

class Clock extends React.Component {
  constructor(props) {
    super(props); // Always call super(props) first!
    // Initialize the state object
    this.state = { date: new Date() };
  }

  render() {
    return (
      <div>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

There are two important things to note here. First, we must call super(props) before any other statement in the constructor. This is required for this to be initialized correctly in a subclass. Second, we set this.state to an object containing the data we want to track. In the render method, we can then access this data using this.state.date.

Updating State Correctly

Our clock currently shows the time it was created, but it doesn't update. To make it tick, we need to change its state. The most important rule of state is to never modify it directly.

Do not modify state directly. For example, this.state.date = new Date() will not re-render the component.

The only way to update a component's state is by using the this.setState() method. Calling setState() tells React that the state has changed. React will then schedule an update, re-rendering the component and its children with the new state.

Let's look at a counter example. When a button is clicked, it should increment a number in the state.

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

  incrementCounter = () => {
    // Use a function inside setState for reliable updates
    this.setState((prevState) => {
      return { count: prevState.count + 1 };
    });
  }

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

Notice how we passed a function to this.setState(). This is the safest way to update state that depends on the previous state. React may batch multiple setState() calls for performance, so using the previous state (prevState) from the function argument ensures your update is based on the latest value, not a stale one.

The Component Lifecycle

Beyond managing state, components also have a "lifecycle." This is a sequence of phases a component goes through from its creation to its destruction. You can tap into these phases by defining special methods, known as lifecycle methods. These let you run code at specific times, like right after the component is added to the screen.

Lesson image

The three main phases are:

  • Mounting: The component is being created and inserted into the DOM.
  • Updating: The component is being re-rendered because of changes to props or state.
  • Unmounting: The component is being removed from the DOM.

Let's focus on two of the most common lifecycle methods to make our clock tick.

componentDidMount

other

A lifecycle method that runs after the component output has been rendered to the DOM. This is a good place to set up subscriptions or make network requests.

We can use componentDidMount() to set up a timer right after our Clock component is first rendered. This timer will call setState() every second to update the time.

componentDidMount() {
  this.timerID = setInterval(
    () => this.tick(),
    1000
  );
}

tick() {
  this.setState({
    date: new Date()
  });
}

This code sets up an interval that calls the tick() method every 1000 milliseconds (or one second). The tick() method then updates the state with the new time, causing the component to re-render and display the correct time.

But what happens when the Clock component is removed? The timer will keep running in the background, which can cause memory leaks and bugs. We need to clean up after ourselves. This is where the unmounting phase comes in.

componentWillUnmount

other

A lifecycle method that runs right before a component is unmounted and destroyed. This is the perfect place to perform any necessary cleanup, such as invalidating timers or canceling network requests.

We'll tear down the timer in the componentWillUnmount() method.

componentWillUnmount() {
  clearInterval(this.timerID);
}

By clearing the interval, we ensure that our component cleans up properly when it's no longer needed. Managing side effects like timers or data fetching within these lifecycle methods is a fundamental part of building robust React applications.

Quiz Questions 1/5

What is the key difference between props and state in a React component?

Quiz Questions 2/5

Which method is the only correct way to update a component's state?

With state and lifecycle methods, class components become truly interactive and dynamic, able to manage their own data and respond to events over time.