Why you should use functional components + Hooks over class components in React

If you're new to React, and you've been working through tutorials, chances are you've run into examples of both functional components with Hooks, and class components, with no strong indication of which one you should be using. Even as a seasoned developer, you might still be using class components, wondering if its worth the rewrite.

You're probably thinking:

I've been digging for answers, but I just can't find a clear answer for this!

That's fair enough, even the official documentation didn't have a strong recommendation until the middle of 2020.

Which one should you use?

The official React team stance (according to the docs), is:

When you’re ready, we’d encourage you to start trying Hooks in new components you write. [...] We don’t recommend rewriting your existing classes to Hooks unless you planned to rewrite them anyway (e.g. to fix bugs).

To summarise:

  • New code should use functional components with Hooks, when you're ready
  • Old code can keep using class components, unless you want to rewrite

Should I just focus on hooks then?

It's not that simple.

You still need class components to build Error Boundaries. Sure, you could head to npm and find a Hook-based Error Boundary library to use, but ignorance doesn't justify adding yet another dependency to your project.

On top of that, most code written before 2019 will likely still use class components, as there is no immediate need to rewrite them to functional components with Hooks. If you want to understand existing code in a codebase, you'll need to also learn class components. Side note: The company I currently work at has class components with >1k lines of code used by tens of thousands of users per day. You better believe we're not rewriting them unless we have to.

You'll also find that companies that ask React questions during their interviews will still ask you about classes.

Should we rewrite our old class-based code to use Hooks?

As with all good things, there are tradeoffs to consider here.

Hooks result in much cleaner, easier to understand components compared to class components of a similar complexity.

To illustrate my point, compare this component that fetches some data from The Star Wars API, written first as a class, then as a functional component with hooks:

import React from 'react';
export default class DataDisplayer extends React.Component {
constructor(props) {
super(props);
this.state = {
data: null,
};
}
async componentDidMount() {
const response = await fetch(
`https://swapi.dev/api/people/${this.props.id}/`
);
const newData = await response.json();
this.setState({ data: newData });
}
render() {
const { data } = this.state;
if (data) {
return <div>{data.name}</div>;
} else {
return null;
}
}
}

As your app grows, the lifecycle methods grow larger, and the context switching involved just from scrolling through the file increases.

I don't know about you, but my thought process when scrolling through classes is like:

Okay so I'm in componentDidMount, so we'll be fetching data here

Okay so here we're rendering, so this code runs every single time

I need to add some extra functionality... hmm which lifecycle method does that go in again?

On the other hand, you have Hooks:

import React, { useEffect, useState } from 'react';
export default function DataDisplayer(props) {
const [data, setData] = useState('');
useEffect(() => {
const getData = async () => {
const response = await fetch(`https://swapi.dev/api/people/${props.id}/`);
const newData = await response.json();
setData(newData);
};
getData();
}, [props.id]);
if (data) {
return <div>{data.name}</div>;
} else {
return null;
}
}

With Hooks, writing code that follows sequentially is much easier, and I find reading functional components with Hooks requires less context switching, as you're not jumping around the file to find which lifecycle method you think something happened in.

That's the main benefit of rewriting to Hooks - your codebase's developer experience improves as it takes less time to understand what each component does.

The main drawback is time - time spent rewriting is time you could have spent building new features or writing integration tests.

Where to from here?

When introducing Hooks to my team in the past I recommended the following approach (we used it to rewrite our entire codebase from Flow to TypeScript), and it worked quite well:

  • All new code should be written as functional components with Hooks
  • Existing code should only be rewritten if it gets modified frequently (for example, if you're fixing a bug or adding functionality, take the time to swap the component over to Hooks)

(Shameless plug for the useEffect book I wrote below)

Tired of infinite re-renders when using useEffect?

A few years ago when I worked at Atlassian, a useEffect bug I wrote took down part of Jira for roughly one hour.

Knowing thousands of customers can't work because of a bug you wrote is a terrible feeling. To save others from making the same mistakes, I wrote a single resource that answers all of your questions about useEffect, after teaching it here on my blog for the last couple of years. It's packed with examples to get you confident writing and refactoring your useEffect code.

In a single afternoon, you'll learn how to fetch data with useEffect, how to use the dependency array, even how to prevent infinite re-renders with useCallback.

Master useEffect, in a single afternoon.

useEffect By Example's book cover