React: Refactoring Class Based Components with Hooks


Chris Held
React: Refactoring Class Based Components with Hooks

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.

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.

React Functional Component

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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:

1
2
3
4
5
6
7
8
9
10
11
12
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:

1
class App extends Component {

Now becomes:

1
const App () => {

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

1
2
3
4
5
6
7
8
9
10
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:

1
2
3
4
5
6
7
8
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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:

1
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.

1
2
3
4
5
6
7
//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.

1
2
3
4
5
6
7
8
9
10
11
12
13
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:

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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.


SIGN UP FOR OUR NEWSLETTER

The Weekly Manifest

Receive the latest design, development, and startup articles to stay updated!

Close

Inquiry Sent!

Thanks so much for your interest in working with us, and for your time to fill out the form. We're passionate about what we do and would love the opportunity to create a successful solution for you.

Expect to hear back from us within the next 3 business days.

Work With Us

We can take on any type of project, but we don’t work with everyone. We only partner with clients that align with our business values, honestly benefit from our expertise, and embrace the systems we build in.

Fill out the form below to start a conversation see if you’re the right fit for us.

What type of project would you like to partner with us on?