Telerik blogs
ReactT2_1200x303

This article covers how to convert common use cases from class components to hooks, so you can modernize your React apps.

React has been around for many years and is often chosen as a solution for creating UIs in modern applications. Throughout the years, the way we write components with React has changed greatly.

Initially, we had the createClass method, which was later replaced by class components. In version 16.8, React released hooks that revolutionized how we write React applications, as they allowed us to write more concise and cleaner code and provided a better pattern for creating reusable stateful logic.

Many developers shifted toward hooks and abandoned class components. However, many legacy React apps still use class components. What’s more, class components still have their use cases, such as error boundaries, as there is no hook for that.

In this article, we will cover how to convert common use cases from class components to hooks.

You can find full code examples in this GitHub repo and an interactive CodeSandbox below.

Managing and Updating Component State

State management is one of the most common things in any React application. React renders components based on the state and props. Whenever they change, components are re-rendered, and the DOM is updated accordingly. Here is an example of a simple class component with a counter state and two methods to update it.

import { Component } from "react";

class ManagingStateClass extends Component {
  state = {
    counter: 0,
  };

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

  decrement = () => {
    this.setState(prevState => {
      return {
        counter: prevState.counter - 1,
      };
    });
  };

  render() {
    return (
      <div>
        <h2>Managing State - Class</h2>
        <div>Count: {this.state.counter}</div>
        <div>
          <button onClick={this.increment}>Increment</button>
          <button onClick={this.decrement}>Decrement</button>
        </div>
      </div>
    );
  }
}

export default ManagingStateClass;

The hooks implementation is much more concise.

import { useState } from "react";

const ManagingStateHooks = () => {
  const [counter, setCounter] = useState(0);

  const increment = () => setCounter(counter => counter + 1);
  const decrement = () => setCounter(counter => counter - 1);

  return (
    <div>
      <h2>Managing State - Hooks</h2>
      <div>Count: {counter}</div>
      <div>
        <button onClick={increment}>Increment</button>
        <button onClick={decrement}>Decrement</button>
      </div>
    </div>
  );
};

export default ManagingStateHooks;

The component is just a function that returns JSX. We use the useState hook to manage the state. It returns an array with two values—the first one is the state and the second one is the updater function. We also have increment and decrement functions that utilize the setCounter updater.

Reacting to State Changes

There are scenarios in which we might need to perform some kind of action whenever the state changes. In a class component, we can do that by using the componentDidUpdate lifecycle.

import { Component } from "react";

class StateChangesClass extends Component {
  state = {
    counter: 0,
  };

  componentDidUpdate(prevProps, prevState) {
    console.log("New counter", this.state.counter);
    localStorage.setItem("counter", this.state.counter);
  }

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

  decrement = () => {
    this.setState(prevState => {
      return {
        counter: prevState.counter - 1,
      };
    });
  };

  render() {
    return (
      <div>
        <h2>Reacting To State Changes - Class</h2>
        <div>Count: {this.state.counter}</div>
        <div>
          <button onClick={this.increment}>Increment</button>
          <button onClick={this.decrement}>Decrement</button>
        </div>
      </div>
    );
  }
}

export default StateChangesClass;

When the state changes, we save the new counter value in the local storage. We can achieve the same in a functional component by utilizing the useEffect hook.

import { useState, useEffect } from "react";

const StateChangesHooks = () => {
  const [counter, setCounter] = useState(0);

  const increment = () => setCounter(counter => counter + 1);
  const decrement = () => setCounter(counter => counter - 1);

  useEffect(() => {
    console.log("Current counter", counter);
    localStorage.setItem("counter", counter);
  }, [counter]);

  return (
    <div>
      <h2>Reacting To State Changes - Hooks</h2>
      <div>Count: {counter}</div>
      <div>
        <button onClick={increment}>Increment</button>
        <button onClick={decrement}>Decrement</button>
      </div>
    </div>
  );
};

export default StateChangesHooks;

The useEffect hook expects two arguments—a callback function and an array of dependencies. This hook always runs at least once after the component is mounted. Then, it runs only when any of the values passed inside of the dependencies array change. If the dependencies array passed to the useEffect is empty, then the effect runs only once. In our example, whenever the counter state changes, the useEffect runs the function that saves the counter in the local storage.

Fetching Data

If you want to fetch some data in class components, you need to initialize an API request in the componentDidMount lifecycle. In the code example below, we fetch and display a list of posts.

import { Component } from "react";

class FetchingDataClass extends Component {
  state = {
    posts: [],
  };

  componentDidMount() {
    this.fetchPosts();
  }

  fetchPosts = async () => {
    const response = await fetch("https://jsonplaceholder.typicode.com/posts");
    const data = await response.json();
    this.setState({
      posts: data.slice(0, 10),
    });
  };

  render() {
    return (
      <div>
        <h2>Fetching Data - Class</h2>
        <div>
          {this.state.posts.map(post => {
            return <div key={post.id}>{post.title}</div>;
          })}
        </div>
      </div>
    );
  }
}

export default FetchingDataClass;

With hooks, we can again use the useEffect hook. As I mentioned previously, the useEffect hook runs once after the component is mounted for the first time, and then any time dependencies passed change. We ensure that the useEffect runs only once by passing an empty array as the second argument for the dependencies argument.

import { useState, useEffect } from "react";

const FetchingDataHooks = () => {
  const [posts, setPosts] = useState([]);

  const fetchPosts = async () => {
    const response = await fetch("https://jsonplaceholder.typicode.com/posts");
    const data = await response.json();
    setPosts(data.slice(0, 10));
  };

  useEffect(() => {
    fetchPosts();
  }, []);

  return (
    <div>
      <h2>Fetching Data - Hooks</h2>
      <div>
        {posts.map(post => {
          return <div key={post.id}>{post.title}</div>;
        })}
      </div>
    </div>
  );
};

export default FetchingDataHooks;

Cleanup When Component Is Unmounted

Cleaning up when a component is unmounted is quite important, as otherwise we could end up with memory leaks. For instance, in a component, we might want to listen to an event like resize or scroll and do something based on the window’s size or scroll’s position. Below you can see a class component example that listens to the resize event and then updates the state with the window’s width and height. The event listener is removed in the componentWillUnmount lifecycle.

import { Component } from "react";

class CleanupClass extends Component {
  state = {
    width: window.innerWidth,
    height: window.innerHeight,
  };

  componentDidMount() {
    window.addEventListener("resize", this.updateWindowSize, {
      passive: true,
    });
  }

  componentWillUnmount() {
    window.removeEventListener("resize", this.updateWindowSize, {
      passive: true,
    });
  }

  updateWindowSize = () => {
    this.setState({
      width: window.innerWidth,
      height: window.innerHeight,
    });
  };

  render() {
    return (
      <div>
        <h2>Cleanup - Class</h2>
        <div>
          Window Size: {this.state.width} x {this.state.height}
        </div>
      </div>
    );
  }
}

export default CleanupClass;

There is one feature of the useEffect hook we did not cover yet. We can perform a cleanup in a component by returning a function from the callback that was passed to the useEffect. This function is called when the component is unmounted. As the example below shows, we first define the updateWindowSize function and then add the resize event listener inside of the useEffect. Next, we return an anonymous arrow function that will remove the listener.

import { useState, useEffect } from "react";

const CleanupHooks = () => {
  const [width, setWidth] = useState(window.innerWidth);
  const [height, setHeight] = useState(window.innerHeight);

  useEffect(() => {
    const updateWindowSize = () => {
      setWidth(window.innerWidth);
      setHeight(window.innerHeight);
    };

    window.addEventListener("resize", updateWindowSize, {
      passive: true,
    });

    return () => {
      window.removeEventListener("resize", this.updateWindowSize, {
        passive: true,
      });
    };
  }, []);

  return (
    <div>
      <h2>Cleanup - Hooks</h2>
      <div>
        Window Size: {width} x {height}
      </div>
    </div>
  );
};

export default CleanupHooks;

Preventing Component From Re-rendering

React is very fast, and usually we don’t have to worry about premature optimization. However, there are cases in which it’s useful to optimize components and make sure they don’t re-render too often.

For instance, a common way of optimizing class components is by either using a PureComponent or the shouldComponentUpdate lifecycle hook. The example below shows two class components—a parent and child. The parent has two stateful values—counter and fruit. The child component should re-render only when the fruit value changes, so we use the shouldComponentUpdate lifecycle to check if the fruit prop changed. If it’s the same, then the child component won’t re-render.

Class parent that causes a re-render

import { Component } from "react";
import PreventRerenderClass from "./PreventRerenderClass.jsx";

function randomInteger(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

const fruits = ["banana", "orange", "apple", "kiwi", "mango"];

class PreventRerenderExample extends Component {
  state = {
    fruit: null,
    counter: 0,
  };

  pickFruit = () => {
    const fruitIdx = randomInteger(0, fruits.length - 1);
    const nextFruit = fruits[fruitIdx];

    this.setState({
      fruit: nextFruit,
    });
  };

  componentDidMount() {
    this.pickFruit();
  }

  render() {
    return (
      <div>
        <h2>Prevent Rerender Class Example</h2>
        <h3>
          Current fruit: {this.state.fruit} | counter: {this.state.counter}
        </h3>

        <button onClick={this.pickFruit}>Pick a fruit</button>
        <button
          onClick={() =>
            this.setState(({ counter }) => ({
              counter: counter + 1,
            }))
          }
        >
          Increment
        </button>
        <button
          onClick={() =>
            this.setState(({ counter }) => ({ counter: counter - 1 }))
          }
        >
          Decrement
        </button>
        <div className="section">
          <PreventRerenderClass fruit={this.state.fruit} />
        </div>
      </div>
    );
  }
}

export default PreventRerenderExample;

Class child with shouldComponentUpdate

import { Component } from "react";

class PreventRerenderClass extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    return this.props.fruit !== nextProps.fruit;
  }

  render() {
    console.log("PreventRerenderClass rendered");
    return (
      <div>
        <p>Fruit: {this.props.fruit}</p>
      </div>
    );
  }
}

export default PreventRerenderClass;

With the introduction of hooks, we got a new higher-order component called memo. It can be used to optimize the performance and prevent functional components from re-rendering. Below we have an implementation with hooks.

Hooks parent that causes a re-render

import { useState, useEffect } from "react";
import PreventRerenderHooks from "./PreventRerenderHooks.jsx";

function randomInteger(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

const fruits = ["banana", "orange", "apple", "kiwi", "mango"];

const PreventRerenderExample = () => {
  const [fruit, setFruit] = useState(null);
  const [counter, setCounter] = useState(0);

  const pickFruit = () => {
    const fruitIdx = randomInteger(0, fruits.length - 1);
    const nextFruit = fruits[fruitIdx];
    setFruit(nextFruit);
  };

  useEffect(() => {
    pickFruit();
  }, []);

  return (
    <div>
      <h2>Prevent Rerender Hooks Example</h2>
      <h3>
        Current fruit: {fruit} | counter: {counter}
      </h3>

      <button onClick={pickFruit}>Pick a fruit</button>
      <button onClick={() => setCounter(counter => counter + 1)}>
        Increment
      </button>
      <button onClick={() => setCounter(counter => counter - 1)}>
        Decrement
      </button>
      <div className="section">
        <PreventRerenderHooks fruit={fruit} />
      </div>
    </div>
  );
};

export default PreventRerenderExample;

Hooks child with memo

import { memo } from "react";

const PreventRerenderHooks = props => {
  console.log("PreventRerenderHooks rendered");
  return (
    <div>
      <p>Fruit: {props.fruit}</p>
    </div>
  );
};

export default memo(PreventRerenderHooks);

The PreventRerenderHooks component is wrapped with the memo component and only re-renders if the fruit prop changes. Note that the memo component performs a shallow comparison under the hood, so if you need more control over when the wrapped component should re-render, you can provide your own function to perform the props comparison.

import { memo } from "react";

const PreventRerenderHooks = props => {
  console.log("PreventRerenderHooks rendered");
  return (
    <div>
      <p>Fruit: {props.fruit}</p>
    </div>
  );
};

export default memo(PreventRerenderHooks, (prevProps, nextProps) => {
  return prevProps.fruit !== nextProps.fruit
});

Context API

Context API is a great tool for providing values to components at different levels in the component hierarchy. A new context can be created by using the createContext method offered by React. For this example, we will have two contexts—one for the user state and the other one for the updater method.

userContext

import { createContext } from "react";

export const UserContext = createContext();
export const UserActionsContext = createContext();

Let’s start with the class component example. In the parent component, we provide the user state and setUser method to consumers.

Class Context Provider

import { Component, createContext } from "react";
import ContextApiClassConsumer from "./ContextApiClassConsumer.jsx";
import { UserContext, UserActionsContext } from "./userContext.js";

class ContextApiHooksProvider extends Component {
  state = {
    user: {
      name: "Thomas Class",
    },
  };

  setUser = user => this.setState({ user });

  render() {
    console.log("in render class user", this.state.user);
    return (
      <UserContext.Provider value={this.state.user}>
        <UserActionsContext.Provider value={this.setUser}>
          <ContextApiClassConsumer />
        </UserActionsContext.Provider>
      </UserContext.Provider>
    );
  }
}

export default ContextApiHooksProvider;

We can consume the context in a class component by utilizing the Context.Consumer component that is available in every context. This component accepts a function as a child that receives context value as an argument.

Class Context Consumer

import { Component } from "react";
import { UserContext, UserActionsContext } from "./userContext.js";

class ContextApiClassConsumer extends Component {
  render() {
    return (
      <UserContext.Consumer>
        {user => (
          <UserActionsContext.Consumer>
            {setUser => (
              <div>
                <h2>ContextApiClass Consumer</h2>
                <input
                  type="text"
                  value={user.name}
                  onChange={e =>
                    setUser({
                      name: e.target.value,
                    })
                  }
                />
              </div>
            )}
          </UserActionsContext.Consumer>
        )}
      </UserContext.Consumer>
    );
  }
}

export default ContextApiClassConsumer;

As the example above shows, the child function of the UserContext.Consumer component receives the user state, and the child function of the UserActionsContext.Consumer receives the setUser method.

The hooks provider example is very similar but much more concise. Again, we use the UserContext.Provider and UserActionsContext.Provider component to provide the user state and the setUser method.

Hooks Context Provider

import { useState } from "react";
import ContextApiHooksConsumer from "./ContextApiHooksConsumer.jsx";
import { UserContext, UserActionsContext } from "./userContext.js";

const ContextApiHooksProvider = () => {
  const [user, setUser] = useState({
    name: "Thomas Hooks",
  });
  return (
    <UserContext.Provider value={user}>
      <UserActionsContext.Provider value={setUser}>
        <ContextApiHooksConsumer />
      </UserActionsContext.Provider>
    </UserContext.Provider>
  );
};

export default ContextApiHooksProvider;

Technically, in a functional component, we could consume the context in the same way as we did in the class component. However, there is a much cleaner approach with hooks, as we can utilize the useContext hook to get access to context values.

Hooks Context Consumer

import { useContext } from "react";
import { UserContext, UserActionsContext } from "./userContext.js";

const ContextApiHooksConsumer = () => {
  const user = useContext(UserContext);
  const setUser = useContext(UserActionsContext);
  return (
    <div>
      <h2>ContextApiHooks Consumer</h2>
      <input
        type="text"
        value={user.name}
        onChange={e =>
          setUser({
            name: e.target.value,
          })
        }
      />
    </div>
  );
};

export default ContextApiHooksConsumer;

If you would like to learn more about how to use Context API in a performant way, I have just the article for you.

Preserving Values Across Re-renders

There are scenarios in which we might need to store some data in a component, but we would not necessarily want to store it in the state, as the UI does not rely on this data in any way.

For instance, we might save some metadata that we would like to include later in an API request. This is very easy to achieve in a class component, as we can just assign a new property to the class.

import { Component } from "react";

class PreservingValuesClass extends Component {
  state = {
    counter: 0,
  };

  componentDidMount() {
    this.valueToPreserve = Math.random();
  }

  showValue = () => {
    alert(this.valueToPreserve);
  };

  increment = () => this.setState(({ counter }) => ({ counter: counter + 1 }));

  render() {
    console.log("PreventRerenderClass rendered");
    return (
      <div>
        <h2>Preserving Values - Class</h2>
        <p>Counter: {this.state.counter}</p>
        <button onClick={this.increment}>Increment</button>
        <button onClick={this.showValue}>Show value</button>
      </div>
    );
  }
}

export default PreservingValuesClass;

In this example, when the component is mounted, we assign a dynamic random number on the valueToPreserve property. We also have the counter increment to force a re-render and the Show value button to show the preserved value in an alert.

Like I said, with a class component, it’s easy, but it’s not so simple in a functional component. The reason for this is because any time a functional component re-renders, everything inside of it has to re-run. What this means is that if we have a component like this:

const MyComponent = props => {
  const valueToPreserve = Math.random()
 	// ... other code
}

The Math.random() method will be called on every re-render, so the first value that was created will be lost.

One way to avoid this problem would be to move the variable outside of the component. This wouldn’t work, though, because if the component was used multiple times, the value would be overridden by each of them.

Fortunately, React provides a hook that is great for this use case. We can preserve values across re-renders in functional components by utilizing the useRef hook.

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

const PreserveValuesHooks = props => {
  const valueToPreserve = useRef(null);
  const [counter, setCounter] = useState(0);

  const increment = () => setCounter(counter => counter + 1);

  const showValue = () => {
    alert(valueToPreserve.current);
  };

  useEffect(() => {
    valueToPreserve.current = Math.random();
  }, []);

  return (
    <div>
      <h2>Preserving Values - Class</h2>
      <p>Counter: {counter}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={showValue}>Show value</button>
    </div>
  );
};

export default PreserveValuesHooks;

The valueToPreserve is a ref that initially starts with the null value. However, it’s later changed in the useEffect to a random number that we want to preserve.

How To Expose State and Methods to a Parent Component

Although we shouldn’t have to access the state and properties of a child component often, there are situations in which it can be useful—for instance, if we want to reset some of the component’s state or get access to its state. We need to create a ref in which we can store a reference to the child component that we want to access. In a class component, we can use the createRef method and then pass that ref to the child component.

Expose Properties Class Parent

import { Component, createRef } from "react";
import ExposePropertiesClassChild from "./ExposePropertiessClassChild";

class ExposePropertiesClassParent extends Component {
  constructor(props) {
    super(props);
    this.childRef = createRef();
  }

  showValues = () => {
    const counter = this.childRef.current.state.counter;
    const multipliedCounter = this.childRef.current.getMultipliedCounter();
    alert(`
      counter: ${counter}
      multipliedCounter: ${multipliedCounter}
    `);
  };

  increment = () => this.setState(({ counter }) => ({ counter: counter + 1 }));

  render() {
    return (
      <div>
        <h2>Expose Properties - Class</h2>
        <button onClick={this.showValues}>Show child values</button>
        <ExposePropertiesClassChild ref={this.childRef} />
      </div>
    );
  }
}

export default ExposePropertiesClassParent;

The showValues method retrieves the counter state and utilizes the getMultipliedCounter method. Below you can see the class child component.

Expose Properties Class Child

import { Component } from "react";

class ExposePropertiesClassChild extends Component {
  state = {
    counter: 0,
  };

  getMultipliedCounter = () => {
    return this.state.counter * 2;
  };

  increment = () => this.setState(({ counter }) => ({ counter: counter + 1 }));

  render() {
    return (
      <div>
        <p>Counter: {this.state.counter}</p>
        <button onClick={this.increment}>Increment</button>
      </div>
    );
  }
}

export default ExposePropertiesClassChild;

To get access to the properties of the child component, we only had to create a ref in the parent component and pass it. Now, let’s have a look at how we can achieve the same thing with functional components and hooks.

Expose Properties Hooks Parent

import { useRef } from "react";
import ExposePropertiesHooksChild from "./ExposePropertiesHooksChild";

const ExposePropertiesHooksParent = props => {
  const childRef = useRef(null);

  const showValues = () => {
    const counter = childRef.current.counter;
    const multipliedCounter = childRef.current.getMultipliedCounter();
    alert(`
      counter: ${counter}
      multipliedCounter: ${multipliedCounter}
    `);
  };

  return (
    <div>
      <h2>Expose Properties - Hooks</h2>
      <button onClick={showValues}>Show child values</button>
      <ExposePropertiesHooksChild ref={childRef} />
    </div>
  );
};

export default ExposePropertiesHooksParent;

In the parent, we use the useRef hook to store a reference to the child component. The value of the childRef is then accessed in the showValues function. As you can see, the implementation is quite similar to the one in the class component.

However, we’re not done yet, as we need to expose properties from the functional component manually. We can do so by utilizing the forwardRef and useImperativeHandle hook.

Expose Properties Hooks Child

import { useState, useImperativeHandle, forwardRef } from "react";

const ExposePropertiesHooksChild = (props, ref) => {
  const [counter, setCounter] = useState(0);

  const increment = () => setCounter(counter => counter + 1);

  useImperativeHandle(ref, () => {
    return {
      counter,
      getMultipliedCounter: () => counter * 2,
    };
  });

  return (
    <div>
      <p>Counter: {counter}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

export default forwardRef(ExposePropertiesHooksChild);

The forwardRef basically forwards the ref passed from the parent to the component, while useImperativeHandle specifies what should be accessible to the parent component.

Summary

I hope that now you have a better idea of how you can convert your class components to hooks. Before you start converting all your components, make sure you go through the official hooks documentation, as there are certain rules that need to be followed, such as the fact that hooks cannot be called conditionally.

After working with hooks for a long time, I can only say that it’s definitely worth mastering them. They provide a lot of benefits, such as more concise code and better stateful logic reusability.


Thomas Findlay-2
About the Author

Thomas Findlay

Thomas Findlay is a 5-star rated mentor, full-stack developer, consultant, technical writer and the author of “React - The Road To Enterprise” and “Vue - The Road To Enterprise.” He works with many different technologies such as JavaScript, Vue, React, React Native, Node.js, Python, PHP and more. Thomas has worked with developers and teams from beginner to advanced and helped them build and scale their applications and products. Check out his Codementor page, and you can also find him on Twitter.

Related Posts

Comments

Comments are disabled in preview mode.