Photo by Anne Nygård on Unsplash

React: Migrating from Class to Functional Components (with hooks)

Timothy Stepro
8 min readJun 29, 2020

--

React is a popular framework. Up until version 16.8, most React components were class based components. I learned and primarily used React class components when I first started with React. Class components were great because they were the only way to use state. With hooks, you can use now use state in functional components.

Because functional components are cleaner to write and easier to read, I believe they’ll help your organization write better web-apps quicker. I also think functional components will lower the barrier for new engineers learning React. Here is how you can transition from class based components to functional ones.

Functional Component

To make sure we’re on the same page, a functional component is just a function that returns JSX. Functional components can render differently based on the props you pass in:

const FunctionalComponent = (props) => {
return (<div> ... </div>);
}

You’ll need to use hooks to do anything interesting with functional components. Hooks are nothing more than functions that are handled by React behind the scenes. They can handle managing state, listening for updates to state, and you can build custom ones. They tend to have a naming convention use*:

const FunctionalComponent = (props) => {
const [x, setX] = useState("Value for x");

return (<div> ... </div>);
}

Using hooks provides several advantages: Simplified state, simplified life-cycle control, and ease of customization.

Simplified State

The useState hook can simplify how we interact with state.

Initializing state in class components looks like this:

class SectionCard extends React.Component {

constructor(props) {
super(props);
this.state = {
title: "How to Article",
content: "This is how you do"
}

}
}

You can consume the state later on in your render function (or lifecycle methods) like so:

class SectionCard extends React.Component {   

constructor(props) {
super(props);
this.state = {
title: "How to Article",
content: "This is how you do"
}
}
render() {
return (
<div>
<h1>{this.state.title}</h1>
<p>{this.state.content}</p>
</div>
);
}
}

You can update state in your component lifecycle methods (or in response to user input like a button clicking).

class SectionCard extends React.Component {   

constructor(props) {
super(props);
this.state = {
title: "How to Article",
content: "This is how you do"
}
}
componentDidMount() {
this.setState({title: "Different title!"});
}
render() {
return (
<div>
<h1>{this.state.title}</h1>
<p>{this.state.content}</p>
</div>
);
}
}

That’s all fine and dandy if you’ve worked with React before 16.8. But there are a few problems with it.

  1. Your component state is being passed around in a map. With many more state fields, your state can difficult manage, debug, and read.
  2. There is extra code to write and track in order to interact with the state. You have to guarantee those values are a part of the state.

So how do React hooks help us? Let’s go through the example above using React Hooks.

We use the useState hook for managing state. This hook takes an initial value of the state and spits out a reference and a setter method for the state value (common naming convention is x, setX):

const [x, setX] = useState("Initial string value X")

These useState hooks set the state for individual fields of our state:

const SectionCard = (props) => {
const [title, setTitle] = useState("How to Article");
const [content, setContent] = useState("This is how you do");
...
}

To use these state fields, we include them when returning our JSX:

const SectionCard = (props) => {
const [title, setTitle] = useState("How to Article");
const [content, setContent] = useState("This is how you do");

...
return (
<div>
<h1>{title}</h1>
<p>{content}</p>
</div>
);
}

And when you want to set the state for either field, you simply use the setters:

const SectionCard = (props) => {
const [title, setTitle] = useState("How to Article");
const [content, setContent] = useState("This is how you do");
useEffect(() => { // Don't worry about useEffect right now
setTitle("Different Title");
}, []);
return (
<div>
<h1>{title}</h1>
<p>{content}</p>
</div>
);
}

Using the useState hooks do several things for you.

  1. They isolate your state to the exact fields you need to set and display.
  2. Following where you interact with your state becomes more clear.
  3. They use less code!
Seeing useState in code

Once you understand the useState hook, the state in your functional component becomes easier to track and work with.

Simplified life cycle

Life cycle methods are called when a component is first mounted, rendered, whether props or state is changed, and when they are unmounted by React. They can only be used in class components.

Life cycle methods that you would use in class components become simplified in functional components with the useEffect hook:

const FunctionalComponent = (props) => {    useEffect(() => {
...
}, ...);
return (
<div>
...
</div>
);
}

useEffect takes a callback and an optional array of values it listens for. The callback will asynchronously fire if one of the values in that array change:

useEffect(
() => {}, // Callback
[...] // Values this hook listens to
);

Also, you can use multiple useEffect hooks if you want to have different callbacks called for different values changing:

const FunctionalComponent = (props) => {

useEffect(() => {
...
}, [...]);
useEffect(() => {
...
}, [...]);
return (
<div>
...
</div>
);
}

Depending on what and if you pass values to listen for, you can emulate different React Class component lifecycle methods.

onComponentDidMount

The onComponentDidMount method is called once in class components when initially rendered. If you pass in an empty list as values to listen for, useEffect will only run once (on the first render) as well:

const FunctionalComponent = (props) => {    useEffect(() => {
// Runs on first render
}, []);
...
}

onComponentWillUnmount

The onComponentWillUnmount method is called in the class component whenever the component is being unmounted and removed. This is typically used to run cleanup tasks. Within the useEffect callback, you can return another callback for the same effect:

const FunctionalComponent = (props) => {

useEffect(() => {
// Runs on first render
return () => {
// Runs once on cleanup/un-mounting this component
};

}, []);
}

onComponentDidUpdate

The onComponentDidUpdate method is called in class components whenever the props or state changes. If you don’t pass in an array of values to listen to, useEffect will do the same:

const FunctionalComponent = (props) => {
...
useEffect(() => {
// Runs on every state change and prop change
});
...
}

This is not efficient and most of the time, you’ll want certain callbacks to get called on specific value changes. This is typically how you’ll structure your useEffect hooks:

const FunctionalComponent = (props) => {
const [a, setA] = useState("State value for a!");
const [b, setB] = useState("State value for b!");
useEffect(() => {
// runs only when a changes
}, [a]);
useEffect(() => {
// runs only when b changes
}, [b]);
...
}

You can also listen to multiple values changing, which will trigger the callback if at least one value in the array changes:

const FunctionalComponent = (props) => {
const [a, setA] = useState("State value for a!");
const [b, setB] = useState("State value for b!");
useEffect(() => {
// runs only when a or b changes
}, [a, b]);
...
}

The useEffect hook did several things for us.

  1. Replaced three class component life cycle methods.
  2. Rather than writing logic for all potential updates (with onComponentDidUpdate), we wrote it for the exact state we’re listening to. This makes our code more modular and I have found it easier to find and fix bugs.

To recap, here are all of the potential uses of useEffect:

const FunctionalComponent = (props) => {
const [a, setA] = useState("State value for a!");
useEffect(() => {
// runs on first render
return () => {
// runs on unmount
}
}, []);
useEffect(() => {
// runs only when a changes
}, [a]);
...
}

Ease of Customization

In some cases, it is wise to abstract common logic out of a single component as you may find it useful in several different ones. You can solve this with higher order components and custom hooks.

With higher order components (HOCs) you end up building a class component that wraps around another component. One HOC I typically end up needing in my projects is a withMobile HOC.

This component is used for hiding particular buttons on mobile and typically looks like this:

const withMobile = (WrappedComponent) => {     return class extends React.Component {        constructor() {
this.state = {
isMobile: false
}
}
onComponentDidMount() {
// I usually attach window listeners.
this.setState({isMobile: window.innerWidth < 640 });
}
render() {
return (
<WrappedComponent isMobile={this.state.isMobile} />
);
}
}

Any component wrapped in withMobile will get access to an isMobile prop:

class SomeComponent extends React.Component {    render() {
return (
<p>Is it Mobile? {this.props.isMobile ? "Yes": "No"}</p>
);
}
}export default withMobile(SomeComponent);

With functional components and hooks, you can still use HOCs but we have an alternative, custom hooks. Let’s create a custom hook, useMobile. This is what it might look like:

const useMobile = () => {
const [isMobile, setIsMobile] = useState(false);

useEffect(() => {
// Again would probably attach listeners here.
setIsMobile(window.innerWidth < 640);
}, []);
return isMobile;
}

And we can use useMobile just like any other hook, without needing to check our props! We can use isMobile similar to how we used it before:

const SomeComponent = (props) => {
const isMobile = useMobile();
return (<p>Is it mobile? {isMobile ? "Yes": "No"} </p>);
}

Custom hooks do several good things for us.

  1. They are modular and they only focus on the logic that can be shared.
  2. We do not have to interact with the child component (we had to pass props to WrappedComponent in our HOC example).
  3. We don’t need to read from props to get the values from our hook.

Summary

Advantages of using functional components with hooks:

  1. They promote more precise code. Easier to think about and write.
  2. They simplify how you interact with state and the life-cycle of components.
  3. They reduce the amount of code to read and write.

Disadvantages of class components:

  1. State is passed around in a map.
  2. Triggers to updated state/props are generic and have to be filtered.
  3. They do not take advantage of Object Oriented principles.
  4. More filler code to support classes can lead to more bugs/frustration.

I hope you see a common theme and the argument for using React Hooks. I argue that they promote cleaner code and I have seen my own productivity increase by using them. I think you will start to see the same benefits if you or your team decide to migrate to React 16.8.

Thank you for taking the time to read this article! If you’d like to learn more about hooks, the React team set up a wonderful set of documentation: https://reactjs.org/docs/hooks-intro.html

--

--

Timothy Stepro

Software engineer by day, software engineer by night.