React: class components vs function components

React: class components vs function components

When I first started working with React, I mostly used function components, especially because I read that class components were old and outdated. But when I started working with React professionally I realised I was wrong. Class components are very much alive and kicking.

So, I decided to write a sort of comparison between class components and function components to have a better understanding of their similarities and differences.

Class components

This is how a class component that makes use of state, props and render looks like:

class Hello extends React.Component {

    constructor(props) {
    super(props);
    this.state = {
      name: props.name
    };
  }

  render() {
    return <h1>Hello, {this.state.name}</h1>;
  }
}

// Render

ReactDOM.render(
  Hello,
  document.getElementById('root')
);

Related sources in which you can find more information about this:

Rendering

Let’s say there is a <div> somewhere in your HTML file:

<div id="root"></div>

We can render an element in the place of the div with root id like this:

const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById('root'));

Regarding React components, we will usually be exporting a component and using it in another file:

  • Hello.jsx
import React, { Component } from 'react';

class Hello extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

export default Hello;
  • main.js
import React from 'react';
import ReactDOM from 'react-dom';
import Hello from './app/Hello.jsx';

ReactDOM.render(<Hello />, document.getElementById('root'));

And this is how a class component gets rendered on the web browser.

Now, there is a difference between rendering and mounting, and Brad Westfall made a great job summarising it:

"Rendering" is any time a function component gets called (or a class-based render method gets called) which returns a set of instructions for creating DOM. "Mounting" is when React "renders" the component for the first time and actually builds the initial DOM from those instructions.

State

A state is a JavaScript object containing information about the component's current condition.

To initialise a class component state we need to use a constructor:

class Hello extends React.Component {

    constructor() {
    this.state = {
      endOfMessage: '!'
    };
  }

  render() {
    return <h1>Hello, {this.props.name} {this.state.endOfMessage}</h1>;
  }
}

Related sources about this:

Caution: we shouldn't modify the state directly because it will not trigger a re-render of the component:

this.state.comment = 'Hello'; // Don't do this

Instead, we should use the setState() method:

this.setState({comment: 'Hello'});

If our current state depends from the previous one, and as setState is asynchronous, we should take into account the previous state:

this.setState(function(prevState, prevProps) {
  return {
    counter: prevState.counter + prevProps.increment
  };
});

Related sources about this:

A common pitfall

If we need to set a state with nested objects, we should spread all the levels of nesting in that object:

this.setState(prevState => ({
    ...prevState,
    someProperty: {
        ...prevState.someProperty,
        someOtherProperty: {
            ...prevState.someProperty.someOtherProperty, 
            anotherProperty: {
               ...prevState.someProperty.someOtherProperty.anotherProperty,
               flag: false
            }
        }
    }
}))

This can become cumbersome, so the use of the [immutability-helper](https://github.com/kolodny/immutability-helper) package is recommended.

Related sources about this:

Before I knew better, I believed that setting a new object property will always preserve the ones that were not set, but that is not true for nested objects (which is kind of logical, because I would be overriding an object with another one). That situation happens when I previously spread the object and then modify one of its properties:

> b = {item1: 'a', item2: {subItem1: 'y', subItem2: 'z'}}
//-> { item1: 'a', item2: {subItem1: 'y', subItem2: 'z'}}
> b.item2 = {...b.item2, subItem1: 'modified'}
//-> { subItem1: 'modified', subItem2: 'z' }
> b
//-> { item1: 'a', item2: { subItem1: 'modified', subItem2: 'z' } }
> b.item2 = {subItem1: 'modified'} // Not OK
//-> { subItem1: 'modified' }
> b
//-> { item1: 'a', item2: { subItem1: 'modified' } }

But when we have nested objects we need to use multiple nested spreads, which turns the code repetitive. That's where the immutability-helper comes to help.

You can find more information about this here.

Props

If we want to access props in the constructor, we need to call the parent class constructor by using super(props):

class Button extends React.Component {
  constructor(props) {
    super(props);
    console.log(props);
    console.log(this.props);
  }
  // ...
}

Related sources about this:

Bear in mind that using props to set an initial state is an anti-pattern of React. In the past, we could have used the componentWillReceiveProps method to do so, but now it's deprecated.

class Hello extends React.Component {

    constructor(props) {
    super(props);

    this.state = {
      property: this.props.name, // Not recommended, but OK if it's just used as seed data.
    };
  }

  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

Using props to initialise a state is not an anti-patter if we make it clear that the prop is only used as seed data for the component's internally-controlled state.

Related sources about this:

Lifecycle methods

Class components don't have hooks; they have lifecycle methods instead.

  • render()
  • componentDidMount()
  • componentDidUpdate()
  • componentWillUnmount()
  • shouldComponentUpdate()
  • static getDerivedStateFromProps()
  • getSnapshotBeforeUpdate()

You can learn more about lifecycle methods here:


Function components

Function components

This is how a function component makes use of props, state and render:

function Welcome(props) {
    const [timeOfDay, setTimeOfDay] = useState('morning');

    return <h1>Hello, {props.name}, good {timeOfDay}</h1>;
}

// or

const Welcome = (props) => {
    const [timeOfDay, setTimeOfDay] = useState('morning');

    return <h1>Hello, {props.name}, good {timeOfDay}</h1>;
}

// Render

const element = <Welcome name="Sara" />;

ReactDOM.render(
  element,
  document.getElementById('root')
);

Rendering

Rendering a function component is achieved the same way as with class components:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Sara" />;

ReactDOM.render(
  element,
  document.getElementById('root')
);

Source:

State

When it comes to the state, function components differ quite a bit from class components. We need to define an array that will have two main elements: the value of the state, and the function to update said state. We then need to assign the useState hook to that array, initialising the state in the process:

import React, { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

The useState hook is the way function components allow us to use a component's state in a similar manner as this.state is used in class components.

Remember: function components use hooks. According to the official documentation:

What is a Hook? A Hook is a special function that lets you “hook into” React features. For example, useState is a Hook that lets you add React state to function components. We’ll learn other Hooks later.

When would I use a Hook? If you write a function component and realize you need to add some state to it, previously you had to convert it to a class. Now you can use a Hook inside the existing function component.

To read the state of the function component we can use the variable we defined when using useState in the function declaration (count in our example).

<p>You clicked {count} times</p>

In class components, we had to do something like this:

<p>You clicked {this.state.count} times</p>

Every time we need to update the state, we should call the function we defined (setCount in this case) with the values of the new state.

<button onClick={() => setCount(count + 1)}>
  Click me
</button>

Meanwhile, in class components we used the this keyword followed by the state and the property to be updated:

<button onClick={() => this.setState({ count: this.state.count + 1 })}>
  Click me
</button>

Sources:

Props

Finally, using props in function components is pretty straight forward: we just pass them as the component argument:

function Avatar(props) {
  return (
    <img className="Avatar"
      src={props.user.avatarUrl}
      alt={props.user.name}
    />
  );
}

Source:

Conclusion

Deciding whether to use class components or function components will depend on the situation. As far as I know, professional environments use class components for "main" components, and function components for smaller, particular components.

I would love to see examples of the use of class and function components in specific situations, so don't be shy of sharing them in the comments section.


🗞️ NEWSLETTER - If you want to hear about my latest articles and interesting software development content, subscribe to my newsletter.

🐦 TWITTER - Follow me on Twitter.

Did you find this article valuable?

Support Damian Demasi's Blog by becoming a sponsor. Any amount is appreciated!