RefactoringPropsAndState

Refactoring Class Components

React introduced Hooks in version 16, enabling function components to replicate the behaviors formerly available only in class components. Despite modern code bases leaning towards function components, you will still encounter class components in legacy projects or partially migrated code. Understanding how to convert class components to function components remains a key skill for every React developer.

Learning Goals

By the end of this guide, you will be able to confidently transition older React class components into the modern function component paradigm, ensuring a smoother codebase and improved readability.

What Are Class Components?

Before React 16, class components were required if you needed component state or lifecycle methods (e.g., componentDidMount, componentWillUnmount). Function components were "stateless" and purely for rendering. With React 16's Hooks, function components can handle state and side effects (via useState, useEffect, etc.), effectively replacing the need for class components in most scenarios.

A class component is defined using JavaScript's class syntax and extends React.Component. For instance:

import React from 'react';

class MyClass extends React.Component {
  render() {
    return <div>Hello from a class!</div>;
  }
}
  

In contrast, a function component looks like this:

function MyFunction() {
  return <div>Hello from a function!</div>;
}
  

Rendering Elements

Class components must define a render method returning the JSX output. Function components simply return JSX. Let’s see a quick transformation:

import React from 'react';

// Class syntax
class ClassComponent extends React.Component {
  render() {
    return (
      <div></div>
    );
  }
}

// Function syntax
function FunctionComponent() {
  return (
    <div></div>
  );
}
  

Component Props

In a class component, props are accessed via this.props. In function components, you typically destructure them in the function parameter list:

// Class component
class ClassComponent extends React.Component {
  render() {
    return <h1>{this.props.title}</h1>;
  }
}

// Function component
function FunctionComponent({ title }) {
  return <h1>{title}</h1>;
}
  

Imagine you have a car assembly line: the "props" represent the raw materials provided to assemble the car. In a class approach, you keep saying "this (car) uses these materials." In a function approach, it’s like the materials are delivered right into the factory function parameters for instant usage.

Constructor And super(props)

You’ll see constructors in class components to initialize the component with props. For example:

class ClassComponent extends React.Component {
  constructor(props) {
    super(props); // must call super with props
  }

  render() {
    return <h1>{this.props.title}</h1>;
  }
}
  

This step ensures this.props is accessible in your class. In a function component, you simply reference title from the function's parameter:

function FunctionComponent({ title }) {
  return <h1>{title}</h1>;
}
  

With Hooks, we no longer need a constructor at all; props are directly passed into the function.

Component State

Class components store their local state in this.state, often initialized in the constructor, and updated using this.setState. Consider this typical example:

class ClassComponent extends React.Component {
  constructor(props) {
    super(props);

    // Initialize component state as an object
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <>
        <h1>{this.props.title}</h1>
        <div>{this.state.count}</div>
        <button onClick={() =>
          this.setState(state => ({ count: state.count + 1 }))
        }>
          Increment
        </button>
      </>
    );
  }
}
  

In the function component world, useState achieves the same outcome without a class or this.

import { useState } from 'react';

function FunctionComponent({ title }) {
  const [count, setCount] = useState(0);

  return (
    <>
      <h1>{title}</h1>
      <div>{count}</div>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>
        Increment
      </button>
    </>
  );
}
  

Think of it like shifting from an old manual "gear box" (class setState) to an automatic "gear lever" (function useState): fewer steps to achieve the same motion, and simpler to manage.

Step By Step Exercise

Let’s do a quick conversion practice to see how a typical refactoring might happen in a real codebase.

Scenario: You have a folder called components in your React src directory that contains counter_class.js, a simple class component that counts how many times it’s been clicked. You want to convert it to a function component named counter_function.js.

File: src/components/counter_class.js

import React from 'react';

class CounterClass extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  render() {
    return (
      <div>
        <h2>{this.props.title}</h2>
        <p>Count: {this.state.count}</p>
        <button onClick={() => this.setState(s => ({ count: s.count + 1 }))}>
          Increment
        </button>
      </div>
    );
  }
}

export default CounterClass;
  

Refactor Steps:

Result: src/components/counter_function.js

import React, { useState } from 'react';

function CounterFunction({ title }) {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h2>{title}</h2>
      <p>Count: {count}</p>
      <button onClick={() => setCount(prev => prev + 1)}>
        Increment
      </button>
    </div>
  );
}

export default CounterFunction;
  

Note how the final file is smaller, with no explicit constructor or this references. The logic is more direct. This is a classic example of refactoring from class to function components.

Metaphor And Real World Example

Converting a class component to a function component is a bit like upgrading from an old style phone to a modern smartphone. The old phone (class component) requires more complicated steps (constructors, this, setState), while the smartphone (function component) uses sleek taps and swipes (useState, direct props usage). They both make calls (render UI), but one is more streamlined in terms of modern capabilities.

In real world teams, you’ll commonly see partial migrations. New code might be function-based, while older code remains in classes. Over time, you or your teammates gradually refactor those older classes into function components for consistency and maintainability.

Why Do This?

As React evolves, the function component + Hooks paradigm is simpler, encourages better separation of concerns, and often results in fewer lines of code. Also, advanced features like custom hooks can reduce duplication across components.

Additional Topics To Explore

Detailed Code Explanations

In a class-based approach, you rely on this to store both props and state. props arrive from the parent, while state is local to the component. By calling super(props) in the constructor, you ensure this.props is valid. Then this.state is an object you can mutate only via this.setState, ensuring React knows to re-render the component.

In the function world, props come in as the function parameter. We store local state with useState, which returns an array of two items: the current state value and a function to set that value. This is more direct and eliminates class boilerplate. You can even have multiple useState calls to track multiple pieces of state separately.

Conclusion

You now know how to identify React class components and convert them into function components that use useState for local state. This shift helps modernize older code bases and improves the simplicity of your components. As you grow more comfortable with Hooks, you’ll find class components less necessary for day-to-day React development.

Whether you’re working on your own project or joining a team with legacy code, these refactoring steps are an essential part of a React developer’s toolkit.