Class components

Class Components

So far we have looked at components which are just functions (which are called function components), but there is another way of creating React components using the class keyword. Let's look at an example (interactive example):

import React, { Component } from "react";

// Class component
class Greeting extends Component {
  render() {
    return <div>Hello</div>;
  }
}

// Function component
const Greeting = () => {
  return <div>Hello</div>;
};

Instead of getting props through the first argument of the component function, the class component gets props from this.props (interactive example):

class Mentor extends Component {
  render() {
    return <div>{this.props.name}</div>;
  }
}

Here are the steps to follow to convert from a function component into a class component:

  1. Import the Component variable by changing the React import to: import React, { Component } from 'react';

  2. Create a new class that extends the component: class MyComponentName extends Component {}

  3. Inside the class, create a render method: render() {}

  4. Copy and paste the contents of the function component into the render method

  5. Replace any references to props with this.props

  6. Delete the old function component

When you should use function components or class components?

Classes are an older method for creating components in React. Even though they are older, they will still continue to work for a long time. Because of this, there are many React components written using classes. You will likely still encounter them when developing with React.

Before Hooks were released classes were the only way to access state and lifecycle (similar to effects). The rule of thumb now is that new components should be written using function components with Hooks, but class components don't need to be updated.

Class Methods

Class methods are similar to nested "handler" functions inside function components. Let's take a look at an example (interactive example):

import React, { Component } from "react";

class Hello extends Component {
  sayHello = () => {
    console.log("Hello from Hello component!");
  };

  render() {
    return <button onClick={this.sayHello}>Say hello</button>;
  }
}

Unlike nested functions, we can't use just the name of the function in the onClick prop. We have to use this first, because the method is attached to the class.

Notice how we use a slightly different syntax for the sayHello method than the render method? There is a reason for this, but it is quite complicated and mostly irrelevant. The rule of thumb is to always use this syntax:

methodName = () => {
  // ...
};

Except for the render method (and a handful of others which we'll talk about later).

State in Class Components

Accessing state in class components is a bit different than with function components. We're going to use the Counter component we looked at previously:

import React, { useState } from "react";

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

  function increment() {
    setCount(count + 1);
  }

  return (
    <div>
      Count: {count}
      <button onClick={increment}>Click me!</button>
    </div>
  );
}

Now let's look at the equivalent version in a class (interactive example):

class Counter extends Component {
  state = {
    count: 0,
  };

  increment = () => {
    this.setState({
      count: 1,
    });
  };

  render() {
    return (
      <div>
        Count: {this.state.count}
        <button onClick={this.increment}>Click me!</button>
      </div>
    );
  }
}

There's a lot going on here! Let's break it down into pieces.

State initialization

Unfortunately, React prevents us from using useState inside class components and we have to use alternatives. To initialize state we have to assign a class property called state:

class Counter extends Component {
  state = {
    count: 0,
  };
}

This creates a new state variable called count. Any value you assign will be the initial value of the state variable, so here we initialize to 0.

Unlike useState, you can initialize multiple state variables at once, like this:

class Counter extends Component {
  state = {
    count: 0,
    isCountingDown: false
  }

  ...
}

Defining the increment handler function

Another difference in the class is that we don't define a "normal" function as the increment handler function. We also don't define it inside the render method, as you might expect.

Instead we define increment as a class method (see above for a reminder):

class Counter extends Component {
  ...

  increment = () => {
    ...
  }

  ...
}

This also means that we have to change how we use the handler function in our JSX:

class Counter extends Component {
  ...

  render() {
    return (
      <div>
        ...
        <button onClick={this.increment}>Click me!</button>
      </div>
    );
  }
}

Accessing state variables

Since we can't use useState, it also means that we don't have access to the count variable. Instead our count state lives inside this.state:

class Counter extends Component {
  ...

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

We have just swapped count with this.state.count. Otherwise it works exactly the same.

Updating state with this.setState

The final piece is how we update state variables. Again, because we can't use useState, we don't have access to setCount. Instead we use a special function called this.setState:

class Counter extends Component {
  ...

  increment = () => {
    this.setState({
      state: 1
    })
  }

  ...
}

This function is similar to setCount because it does two things:

  1. Updates the state variable

  2. Triggers a re-render

One difference is that this.setState can update multiple state variables at once:

this.setState({
  state: 1,
  isCountingDown: true,
});

Updating state based on previous state

You may have noticed that our Counter app isn't very useful. It can only count to 1! This is because this.setState has one more difference: if you need to update some state based on the previous state, then we need to pass a callback instead of an object (interactive example):

class Counter extends Component {
  state = {
    count: 0,
  };

  increment = () => {
    this.setState((previousState) => {
      return {
        count: previousState.count + 1,
      };
    });
  };

  render() {
    return (
      <div>
        Count: {this.state.count}
        <button onClick={this.increment}>Click me!</button>
      </div>
    );
  }
}

Now we can count up as much as we like!

This is because React can "delay" this.setState executing for performance reasons. By using a callback function, we ensure that we are computing the new state with the correct version of the old state and not an outdated one. This problem is one of the reasons why useState was created.

Recap

Let's recap what we've learnt about React state:

  • We initialize state by assigning a state class property to an object with whatever initial state we want (e.g. { something: 'hello' })

  • We can read or render state by using the this.state variable (e.g. this.state.something)

  • We can change state using the this.setState() method and by passing the piece of state we want to update (e.g. this.setState({ something: 'hi' }))

  • If we need to read the previous state to be able to calculate the new state, then we must use a callback function with this.setState() (e.g. this.setState((previousState) => { return { something: previousState.something + 1 } }))

Unmounting

So far we've looked at components that are always rendered in the browser. However (and this is often the case in large applications), we might want to control whether components are shown or not. Let's look at a Toggle component (interactive example):

const Message = () => <p>I'm shown when this.state.isShown is true ✅</p>;

class Toggle extends Component {
  state = {
    isShown: false,
  };

  toggle = () => {
    this.setState((previousState) => {
      return { isShown: !previousState.isShown };
    });
  };

  render() {
    return (
      <div>
        {this.state.isShown ? <Message /> : null}
        <button onClick={this.toggle}>Toggle</button>
      </div>
    );
  }
}

If you open up dev tools, you will see that the element changes based on the isShown state. The hidden element is not hidden with CSS, it is actually removed from the DOM. This is because this.state.isShown is false which means the Toggle component returns null for that part of the JSX. If you return null in JSX then React will render nothing at all.

Class Component Lifecycle

When a class component is within the DOM, we call it mounted. When a component is removed from the DOM, we call it unmounted. When we change state like in the unmounting example above, we can switch between these statuses. This gives us a clue that components go through a lifecycle of different statuses. We have seen 2 of the statuses: mounting and unmounting, there is also a third called updating.

We can hook into this lifecycle through special component methods that are added by React's Component class. They are run at different points of the lifecycle, often before and after they change to a different status. The method names contain will or did based on whether they run before or after a status change.

This diagram shows the React component lifecycle:

Let's look at how we can use one of the lifecycle methods (interactive example):

class Lifecycle extends Component {
  componentDidMount() {
    console.log("componentDidMount");
  }

  render() {
    return <div>Hello World</div>;
  }
}

We'll now focus on a few of the lifecycle hooks and see how they are used.

componentDidMount and componentWillUnmount

The componentDidMount method runs after a component has finished rendering to the DOM. The component is now waiting for a props change or input from the user. It is called only once. We use this lifecycle hook to make changes outside of the component (sometimes these are called side effects).

The componentWillUnmount method runs when a component has been unmounted from the DOM. It is used to "clean up" the component as it is no longer being shown. Often we need to close down or cancel the changes we made in componentDidMount.

To look at these in more detail, we'll create a Clock component in an exercise.

Fetching Data with Class Components

Data fetching with class components is also a bit different than with Hooks. The problem is similar to accessing state, because we can't use useEffect inside our class components. Instead we can use the class component lifecycle.

The component lifecycle is very important - we don't want to be calling our API at the wrong time, or multiple times with the same data! If we tried to fetch data in our render method, it would make a request every time props or state changed. This would create lots of unnecessary requests. A similar problem is solved when using useEffect by the dependencies array.

As we saw above, componentDidMount is called only once when the component is first rendered and so it is an ideal place for making requests. Let's look at an example (interactive example):

class MartianPhotoFetcher extends Component {
  componentDidMount() {
    fetch(
      `https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos?earth_date=${this.props.date}`
    );
  }

  render() {
    // We don't don't what the img src is when we render :(
    return <img src={src} />;
  }
}

This example isn't very useful! We can't use the data returned from the server in render because the request is asynchronous :( We need React to re-render once the request is resolved - a perfect use for state! Let's look at an example (interactive example)

class MartianPhotoFetcher extends Component {
  state = {
    imgSrc: null,
  };

  componentDidMount() {
    fetch(
      `https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos?earth_date=${this.props.date}`
    )
      .then((res) => res.json())
      .then((data) => {
        this.setState({
          imgSrc: data.photos[0].img_src,
        });
      });
  }

  render() {
    return <img src={this.state.imgSrc} />;
  }
}

Now we can see the Martian photo that we fetched from the server!

However we have a bit of a problem - when we first render the component, we don't have the photo src yet. We first have to initialize it to null. This shows us that we're missing something from our UI - a loading status.

Let's look at showing a different UI when the request is loading (interactive example):

class MartianPhotoFetcher extends Component {
  state = {
    isLoading: true,
    imgSrc: null,
  };

  componentDidMount() {
    fetch(
      `https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos?earth_date=${this.props.date}&api_key=gnesiqnKCJMm8UTYZYi86ZA5RAnrO4TAR9gDstVb`
    )
      .then((res) => res.json())
      .then((data) => {
        this.setState({
          isLoading: false,
          imgSrc: data.photos[0].img_src,
        });
      });
  }

  render() {
    if (this.state.isLoading) {
      return <span>Loading... 👽</span>;
    } else {
      return <img src={this.state.imgSrc} />;
    }
  }
}

Here are the steps that the component takes:

  • Initialize isLoading to true

  • In render, show a loading message because isLoading is true

  • Once rendered, componentDidMount will trigger the API request

  • When the request resolves, we set the isLoading state to false and set the data that we want

  • Changing state triggers a re-render, and because isLoading is false we render the Martian photo

We can still improve our component! What happens if we make a request that fails? Our request will error, but we won't show the error in the browser. Let's see how we can fix it (interactive example).

First we have to deal with annoying quirk of fetch. You may remember that it doesn't reject the promise on HTTP errors. The fix is to add another .then before we convert to JSON:

.then((res) => {
  if (res.ok) {
    return res;
  } else {
    throw new Error('HTTP error');
  }
})

Now we can add our solution - a .catch on the fetch call. Here we reset the loading state and add the error to state.

.catch((err) => {
  this.setState({
    isLoading: false,
    err: err
  });
})

Now we can check if there's an error in state and render out an error message:

render() {
  if (this.state.isLoading) {
    return <span>Loading... 👽</span>;
  } else if (this.state.error) {
    return <span>Something went wrong 😭</span>;
  } else {
    return <img src={this.state.imgSrc} />;
  }
}

Further Reading

Last updated