What We Do

Company

Resources

Events

Blog

Free Consultation

ahoy@headway.io

(920) 309 - 5605

4 min
React: Refactoring Class Based Components with Hooks
Subscribe

React: Refactoring Class Based Components with Hooks

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

Asking Better Questions About Your Product

Download our free guide to begin implementing feedback loops in your organization.

By filling out this form, you agree to receive marketing emails from Headway.

Scaling products and teams is hard.

In this free video series, learn how the best startup growth teams overcome common challenges and make impact.

Scaling products and teams is hard.

In this free video series, learn how the best startup growth teams overcome common challenges and make impact.

You don’t need developers to launch your startup

In this free video series, learn proven tactics that will impact real business growth.

By filling out this form, you agree to receive marketing emails from Headway.

Make better decisions for your product

Dive deeper into the MoSCoW process to be more effective with your team.

By filling out this form, you agree to receive marketing emails from Headway.

A mindset for startup growth

In this free video series, learn the common mistakes we see and give yourself a greater chance for success.

By filling out this form, you agree to receive marketing emails from Headway.

The ultimate UX audit kit

Everything you need for a killer DIY audit on your product.

  • UX Audit Guide with Checklist
  • UX Audit Template for Figma
  • UX Audit Report Template for Figma

Enjoyed this post?

Other related posts

See all the ways we can help you grow through design, development, marketing, and more.

View All

Listen and learn from anywhere

Listen and learn from anywhere

Listen and learn from anywhere

The Manifest

Level up your skills and develop a startup mindset.
Stay up to date with the latest content from the Headway team.