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

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

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](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).

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.