You are comfortable with class components and you are wondering if you should learn React hooks (short answer: yes, you should).

Don’t worry about losing your job if you don’t learn hooks because class components won’t be deprecated and there is no need to update all codebases to hooks.

However, hooks are a thing. They may look over-hyped, but they are important specially when starting something from scratch.

I know that feeling when you’re comfortable with a technology and you’ve to learn a new toy (I had to learn Node when I was used to Ruby on Rails 😭)

Ah shit, here we go again

You deciding to learn hooks

In this post we are going to use what you already know to kick-off your hooks journey. I know that your brain works well with classes, so lets take this advantage!

Refactoring a simple component

One of the first things we learn in React (classes) is the component lifecycle: constructor, componentDidMount, componentWillUnmoun and render.

Let’s use them and create a “you have been X seconds here” example.

This app will show how long have you been in the page. To build this, we will:

  1. When the component is initialized, we initialize a state with counter = 0. We do this in the constructor.
  2. After the component is mounted, we start a timer interval that increases the counter by one every 1000 milliseconds (1 second). We do this in componentDidMount.
  3. Before the component is unmounted, we clear the timeout. We do this in componentWillUnmount.

Here is the implementation of this app:

import React from "react";

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = { counter: 0 };
  }

  componentDidMount() {
    this.intervalID = setInterval(() => {
      this.setState(state => ({
        counter: state.counter + 1
      }));
    }, 1000);
  }

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

  render() {
    const { counter } = this.state;
    return (
      <div style={{ textAlign: "center" }}>
        <h1>Hey</h1>
        <div>You have been there for {counter} seconds.</div>
      </div>
    );
  }
}
Demo

I hope you don’t see anything strange here. It uses class component with the classic component lifecycle methods.

Now I’ll show you how to refactor it to hooks.

Step 1: transform your class component into a function component

In other words:

  1. Replace class for function
  2. Remove extends React.Component
  3. Remove everything inside except the render code

In our case:

import React from "react";

export default function App() {
  const { counter } = this.state;
  return (
    <div style={{ textAlign: "center" }}>
      <h1>Hey</h1>
      <div>You have been there for {counter} seconds.</div>
    </div>
  );
}

This is not working yet. We have to modify it a bit…

Step 2: state management with useState

You know how to use state in class components:

  • Initialize state in the constructor.
  • Update it using setState method which triggers a new render.
  • Read state properties from this.state

In functional components, the state works similar:

  • it keeps values between renders
  • updating it triggers a new render

However, in function components, you don’t have a constructor and you don’t have instance methods such as setState.

So the mechanics here are a bit different. To use state in a functional component, you have to use a hook, which is a function that gives superpowers to your functional components.

The hook that brings state superpower to our function components is called useState hook

export default function FunctionalComponentWithState() {
  const someState = React.useState(initialValue); // <-- state superpower!!
  // ...
}
Using useState hook

By calling useState, you are creating a single piece of state associated to the component. It is similar to initializing the state in the constructor of a class component.

useState returns two things:

  1. the current value of the state variable
  2. a function to update it:
function ComponentWithState() {
  const [currentValue, updateValue] = React.useState(initialValue);
}
Creating a piece of state, getting the current value and a function to update it. Magic! 🤯

Well, technically useState returns an array of size two where the first element is the current value and the second element is a function to update it.

Lets go back to the example. We need to store the counter, so instead of

  const { counter } = this.state;
Reading state in a class component

We should use:

const [counter, setCounter] = useState(0); // 0 is the initial value
In one line we tell react to create a state variable with an initial value, get the current value and a function to update it.

Now our App code should look like this:

import React, { useState } from "react";
    
export default function App() {
  const [counter, setCounter] = useState(0);
  return (
    <div style={{ textAlign: "center" }}>
      <h1>Hey</h1>
      <div>You have been there for {counter} seconds.</div>
    </div>
  );
}
Functional component with state

This works! But we are not done yet because we are not counting every second.

Step 3: implement componentDidMount with useEffect hook

In function components, to execute code after the component has been rendered you have to use another superpower (hook) called useEffect hook.

The useEffect hook takes a function which will be invoked after render. Very similar to componentDidMount:

import React, { useState, useEffect } from "react";

export default function App() {
  const [counter, setCounter] = useState(0);
  
  useEffect(() => {
    // "componentDidMount" logic should be here
  }, []);
  
  return (
    <div style={{ textAlign: "center" }}>
      <h1>Hey</h1>
      <div>You have been there for {counter} seconds.</div>
    </div>
  );
}
Adding componentDidMount logic

After render (in our useEffect) we have to:

  1. start the timer (setTimeout).
  2. update the counter on “every tick”.

Instead of using setState we should use setCounter. The state update logic:

this.setState(state => ({
  counter: state.counter + 1
}));
Code to update state in class components

is replaced with:

setCounter(value => value + 1);
Update state with hooks

When setCounter is called, it will trigger a new render, similar to calling this.setState in the class component.

Now our App looks like this:

import React, { useState, useEffect } from "react";

export default function App() {
  const [counter, setCounter] = useState(0);
  
  useEffect(() => {
    setInterval(() => {
      setCounter(oldCounterValue => oldCounterValue + 1)
    }, 1000);
  }, []);
  
  return (
    <div style={{ textAlign: "center" }}>
      <h1>Hey</h1>
      <div>You have been there for {counter} seconds.</div>
    </div>
  );
}

Which does almost everything!

I said before that the function you pass to useEffect is invoked after render. That is different than after mount. The empty array is used to tell React to invoke only once (on mount!)

useEffect(() => {
  setInterval(() => {
    setCounter(oldCounterValue => oldCounterValue + 1)
  }, 1000);
}, []); // <--- HERE
useEffect that runs only once

Step 4: implement componentWillUnmount with useEffect’s cleanup function

useEffect can return a function that cleans up effects from the previous render, which can be used to remove subscriptions (for example, to call clearInterval).

If your effect runs only on “mount” (like our component), the cleanup logic will run only on unmount. Our updated useEffect looks like this:

useEffect(() => {
  cont intervalId = setInterval(() => {
    setCounter(oldCounterValue => oldCounterValue + 1)
  }, 1000);
  
  return () => { // <--- returns a "cleanup" function
    // "on unmount"
    clearInterval(intervalId);
  }
  
}, []);
useEffect with "on unmount" logic

The final code

And the final code:

import React, { useState, useEffect } from "react";

export default function App() {
  const [counter, setCounter] = useState(0);
  
  useEffect(() => {
    const intervalId = setInterval(() => {
      setCounter(oldCounterValue => oldCounterValue + 1)
    }, 1000);

    return () => clearInterval(intervalId);
  }, []);
  
  return (
    <div style={{ textAlign: "center" }}>
      <h1>Hey</h1>
      <div>You have been there for {counter} seconds.</div>
    </div>
  );
}
See in codesandbox

Recap

useState instead of this.state = { … }

useState asks for an initial value, and returns the current value and a function to update it. All in a single line of code:

const [currentValue, setValue] = useState(initialValue);
Component state management with useState hook

Calling setValue will trigger a new render, just like using this.setState.

useEffect instead of componentDidMount / componentWillUnmount

useEffect takes a function which is invoked after render. This is generally used to run code with side effects such as HTTP requests, subscriptions, etc. Similar to what you do in componentDidMount.

You can tell react to run cleanup logic (removing a subscription for example) that you added in your effect by making your effect return a function.

useEffect(() => {
  // on mount: do something
  return () => {
    // on unmount: do something
  }
}, []); // <--- empty array = tell react to run it once
useEffect that imitates componentDidMount and componentWillUnmount

Too much to remember? Don’t worry, I made a Class components to Hooks Cheatsheet you can use 🎁 💝

Get it below 👇