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.
[React version 16.8](https://reactjs.org/blog/2019/02/06/react-v16.8.0.html) 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.
## 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](https://whosnatchedit.herokuapp.com/) is a hosted version of the application we are converting, and you can look at the class component based version of the code [here](https://github.com/chris-held/whosnatchedit/tree/no-hooks).
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:**
-- CODE line-numbers language-jsx --
<!--
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:**
-- CODE line-numbers language-jsx --
<!--
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:**
-- CODE line-numbers language-jsx --
<!--
class App extends Component {
-->
**Now becomes:**
-- CODE line-numbers language-jsx --
<!--
const App () => {
-->
Our Header component behaves similarly, but it has the added wrinkle of extending `PureComponent` rather than `Component` to prevent unneccesary re-rendering:
-- CODE line-numbers language-jsx --
<!--
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](https://reactjs.org/docs/react-api.html#reactmemo):
-- CODE line-numbers language-jsx --
<!--
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:**
-- CODE line-numbers language-jsx --
<!--
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:**
-- CODE line-numbers language-jsx --
<!--
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.
-- CODE line-numbers language-jsx --
<!--
//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`.
-- CODE line-numbers language-jsx --
<!--
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:**
-- CODE line-numbers language-jsx --
<!--
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:
-- CODE line-numbers language-jsx --
<!--
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:**
-- CODE line-numbers language-jsx --
<!--
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.
-- CODE line-numbers language-jsx --
<!--
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:**
-- CODE line-numbers language-jsx --
<!--
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](https://github.com/chris-held/whosnatchedit/tree/no-hooks), and a comparison of the two codebases [here](https://github.com/chris-held/whosnatchedit/compare/no-hooks...hooks). If you'd like to learn more about Hooks, I would recommend checking out the React team's documentation [here](https://reactjs.org/docs/hooks-intro.html).