Development

React: Refactoring Class Based Components with Hooks

Learn how React Hooks can be used to clean up and improve existing code by refactoring class-based components. In this tutorial, we'll be converting a small application created using `create-react-app` that keeps track of what a user has borrowed out and to whom.

4 min
March 7, 2019
Chris Held
Development Lead

Why functional components over class based ones?

Classes are bigger

Functional components are smaller. In terms of file size, this may seem insignificant in smaller projects, but in larger applications this can result in a decent reduction in bundle size, which will improve the performance of your application.

Combine code in a meaningful way

In classes, things that should be tied together are forced to be split apart, and things that should be split apart are forced together. Take an event emitter for example. In a class based component, creating the emitter takes place in componentDidMount and is cleaned up in componentWillUnmount. That same mount function may also initialize some state or create additional emitters. This can quickly become complex and hard to read - with Hooks, we're able to combine our code in a more meaningful way.

Reusing stateful logic accross components

Hooks are just functions, so this means stateful logic can easily be exported and reused. With class components this often requires lots of nesting or using mixins, and in some cases code has to be repeated.

developer typing on macbook pro at desk

How to improve existing code with Hooks

To demonstrate how Hooks can be used to clean up and improve existing code, we'll be converting a small application created using create-react-app that keeps track of what a user has borrowed out and to whom. Here is a hosted version of the application we are converting, and you can look at the class component based version of the code here.

The app only contains a few components: the App component that create react app creates by default, a Header, a ListContainer component responsible for fetching our data, and a List component responsible for showing our data. We also have a few tests around our List component to help keep us honest as we make changes. To keep things simple, the app reads from and writes to local storage.

The App.js and ListContainer.js files are really pretty straightforward to convert to functional components. We don't even need to use Hooks.

Here they are as class components

Here they are as class components:

class App extends Component {
  render() {
    return (
      <div className="main">
        <Header />
        <ListContainer />
      </div>
    );
  }
}

class ListContainer extends Component {
  render() {
    const raw = window.localStorage.getItem("items");
    const items = raw && raw.length ? JSON.parse(raw) : [];
    return <List items={items} />;
  }
}


And here they are as functional components

const App = () => (
  <div className="main">
    <Header />
    <ListContainer />
  </div>
);

const ListContainer = () => {
  const raw = window.localStorage.getItem("items");
  const items = raw && raw.length ? JSON.parse(raw) : [];
  return <List initialItems={items} />;
};


As you can see, there's not much difference between the functional components and the class based ones. Functional components don't have a `render` function, they simply return the component.

The first line of the components are also different

class App extends Component {


Now becomes:

const App () => {


Our Header component behaves similarly, but it has the added wrinkle of extending `PureComponent` rather than `Component` to prevent unneccesary re-rendering:

class Header extends PureComponent {
  render() {
    return (
      <div>
        <h1>Who Snatched It?</h1>
        <p className="lead">The holistic guide to who was your stuff.</p>
      </div>
    );
  }
}


To emulate this in a functional component, we use React.memo:

import React, { memo } from "react";

const Header = memo(() => (
  <div>
    <h1>Who Snatched it?</h1>
    <p className="lead">The holistic guide to who was your stuff.</p>
  </div>
));


Similar to our previous examples, we change the first line to create a function instead of a class and remove the `render` function. In this example, however, we wrap our function in the `memo` function.

Our List component is where we will start to see some of the benefits of using Hooks.

Here is our class based List component

class List extends Component {
  state = {
    items: []
  };

  interval;

  componentDidMount = () => {
    const { items } = this.props;
    this.setState({ items });
    this.interval = setInterval(() => {
      const { items } = this.state;
      console.log("items", items);
      window.localStorage.setItem("items", JSON.stringify(items));
    }, 3000);
  };

  addItem = () => {
    const { items } = this.state;
    this.setState({
      items: [{ who: "", what: "", id: new Date().getTime() }, ...items]
    });
  };

  removeItem = id => {
    const items = Array.from(this.state.items);
    this.setState({
      items: items.filter(i => i.id !== id)
    });
  };

  handleChange = (index, prop, value) => {
    const items = Array.from(this.state.items);
    items[index][prop] = value;
    this.setState({ items });
  };

  componentWillUnmount = () => {
    clearInterval(this.interval);
  };

  render() {
    const { items } = this.state;
    return (
      <React.Fragment>
        <div className="row">
          <button
            onClick={this.addItem}
            data-testid="List.Add"
            className="btn btn-primary btn-lg"
          >
            Add
          </button>
        </div>
        <React.Fragment>
          {items.map((item, index) => (
            <div className="row" key={item.id} data-testid="ListItem">
              <form>
                <input
                  type="text"
                  data-testid="ListItem.WhoInput"
                  className="input-small"
                  onChange={e => {
                    this.handleChange(index, "who", e.target.value);
                  }}
                  value={item.who}
                  placeholder="Who"
                />
                <input
                  type="text"
                  className="input-small"
                  onChange={e => {
                    this.handleChange(index, "what", e.target.value);
                  }}
                  value={item.what}
                  placeholder="What"
                />
                <button
                  type="button"
                  data-testid="ListItem.Remove"
                  onClick={() => {
                    this.removeItem(item.id);
                  }}
                  className="btn btn-outline-danger"
                >
                  X
                </button>
              </form>
            </div>
          ))}
        </React.Fragment>
      </React.Fragment>
    );
  }
}


There's a lot going on here, so let's convert this file step by step.

First we'll update our component to be functional

const List = ({items: initialItems}) => {

We're using destructuring to get our `items` prop, and renaming it to `initialItems`. We'll make use of that variable in the next line. Since we're no longer a class we need to remove our class level variable `state` and replace it. This is where `useState` comes in.

//before
state = {
  items: []
};

//after
const [items, setItems] = useState(initialItems);


Here we're using `useState` to initialize our `items` array to the value of `initialItems`. The return value is an array that contains our value and a function to update that state. We were previously setting initial state in our componentDidMount lifecycle event, but we're going to want to remove those as well with `useEffect`.

componentDidMount = () => {
  const { items } = this.props;
  this.setState({ items });
  this.interval = setInterval(() => {
    const { items } = this.state;
    console.log("items", items);
    window.localStorage.setItem("items", JSON.stringify(items));
  }, 3000);
};

componentWillUnmount = () => {
  clearInterval(this.interval);
};


Becomes:

useEffect(() => {
  const interval = setInterval(() => {
    console.log("items", items);
    window.localStorage.setItem("items", JSON.stringify(items));
  }, 3000);
  return () => {
    clearInterval(interval);
  };
});


We were able to clean this code up quite a bit, and couple our set and clear interval functions to make this function a little more clear. Since `items` is defined once in our `useState` function we no longer need to worry about fetching it from `props` or `state` every time we call a function.

We also need to convert the functions that are managing our state.
We once again make use of `useState` here:

addItem = () => {
  const { items } = this.state;
  this.setState({
    items: [{ who: "", what: "", id: new Date().getTime() }, ...items]
  });
};

removeItem = id => {
  const items = Array.from(this.state.items);
  this.setState({
    items: items.filter(i => i.id !== id)
  });
};

handleChange = (index, prop, value) => {
  const items = Array.from(this.state.items);
  items[index][prop] = value;
  this.setState({ items });
};


Becomes:

const addItem = () => {
  setItems([{ who: "", what: "", id: new Date().getTime() }, ...items]);
};

const removeItem = id => {
  const copy = Array.from(items);
  setItems(copy.filter(i => i.id !== id));
};

const handleChange = (index, prop, value) => {
  const copy = Array.from(items);
  copy[index][prop] = value;
  setItems(copy);
};


These look very similar, with the main difference being we're no longer using `this.state` and we've replaced our `setState` calls with the `setItems` function returned from `useState`.

Lastly, since we're no longer using a class we have to remove the render function and return our component directly. We'll also be removing any instances we find of `this.` in our component.

return (
  <React.Fragment>
    <div className="row">
      <button
        onClick={addItem}
        data-testid="List.Add"
        className="btn btn-primary btn-lg"
      >
        Add
      </button>
    </div>
    <React.Fragment>
      {items.map((item, index) => (
        <div className="row" key={item.id} data-testid="ListItem">
          <form>
            <input
              type="text"
              data-testid="ListItem.WhoInput"
              className="input-small"
              onChange={e => {
                handleChange(index, "who", e.target.value);
              }}
              value={item.who}
              placeholder="Who"
            />
            <input
              type="text"
              className="input-small"
              onChange={e => {
                handleChange(index, "what", e.target.value);
              }}
              value={item.what}
              placeholder="What"
            />
            <button
              type="button"
              data-testid="ListItem.Remove"
              onClick={() => {
                removeItem(item.id);
              }}
              className="btn btn-outline-danger"
            >
              X
            </button>
          </form>
        </div>
      ))}
    </React.Fragment>
  </React.Fragment>
);


Here's our final List component migrated over to a functional component with Hooks

const List = ({ items: initialItems }) => {
  const [items, setItems] = useState(initialItems);

  const addItem = () => {
    setItems([{ who: "", what: "", id: new Date().getTime() }, ...items]);
  };

  const removeItem = id => {
    const copy = Array.from(items);
    setItems(copy.filter(i => i.id !== id));
  };

  const handleChange = (index, prop, value) => {
    const copy = Array.from(items);
    copy[index][prop] = value;
    setItems(copy);
  };

  useEffect(() => {
    const interval = setInterval(() => {
      console.log("items", items);
      window.localStorage.setItem("items", JSON.stringify(items));
    }, 3000);
    return () => {
      clearInterval(interval);
    };
  });
  return (
    <React.Fragment>
      <div className="row">
        <button
          onClick={addItem}
          data-testid="List.Add"
          className="btn btn-primary btn-lg"
        >
          Add
        </button>
      </div>
      <React.Fragment>
        {items.map((item, index) => (
          <div className="row" key={item.id} data-testid="ListItem">
            <form>
              <input
                type="text"
                data-testid="ListItem.WhoInput"
                className="input-small"
                onChange={e => {
                  handleChange(index, "who", e.target.value);
                }}
                value={item.who}
                placeholder="Who"
              />
              <input
                type="text"
                className="input-small"
                onChange={e => {
                  handleChange(index, "what", e.target.value);
                }}
                value={item.what}
                placeholder="What"
              />
              <button
                type="button"
                data-testid="ListItem.Remove"
                onClick={() => {
                  removeItem(item.id);
                }}
                className="btn btn-outline-danger"
              >
                X
              </button>
            </form>
          </div>
        ))}
      </React.Fragment>
    </React.Fragment>
  );
};


Learn more about React Hooks

Even in this small application, converting to functional components has lowered our file sizes and improved the readability of the application. If you'd like to look further, you can find the finished version of the app here, and a comparison of the two codebases here.

If you'd like to learn more about Hooks, I would recommend checking out the React team's documentation here.React version 16.8 has been released and with it comes the exciting new Hooks feature. Previously, any time you needed a component to manage state, you were forced to use a class based component. With Hooks, you are able to write your application using only functional components.

Actionable UX audit kit

  • Guide with Checklist
  • UX Audit Template for Figma
  • UX Audit Report Template for Figma
  • Walkthrough Video
By filling out this form you agree to receive our super helpful design newsletter and announcements from the Headway design crew.

Create better products in just 10 minutes per week

Learn how to launch and grow products with less chaos.

See what our crew shares inside our private slack channels to stay on top of industry trends.

By filling out this form you agree to receive a super helpful weekly newsletter and announcements from the Headway crew.