React has evolved significantly over the years, and one of the major changes in version 16.8.0 is the introduction of hooks, which allow you to use state and other React features without writing a class. In this guide, we will walk through migrating various types of class components to functional components.
Functional components existed before but with an introduction of hooks - they look absolutely different.
What Are Class Components
Class components are ES6 JavaScript classes that extend from React.Component
and implement a render method, which returns React elements (JSX).
Here's a basic example:
jsxclass TextComponent extends React.Component { render() { return <h1>Hello, World!</h1>; } }
State in Class Components
Class components can have state, a built-in object that allows components to manage their internal data. State is local to the component and can change over time.
Let's create a Counter
component that manages count
variable in the state.
The component initializes its state in the constructor and provides a method to update the state using this.setState
:
jsximport React from "react"; class Counter extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } increment = () => { this.setState({ count: this.state.count + 1 }); }; render() { return ( <div> <p>Count: {this.state.count}</p> <button onClick={this.increment}>Increment</button> </div> ); } }
Props in Class Components
Props (short for properties) are used to pass data from parent components to child components. Props are read-only and cannot be modified by the child component unlike a state.
jsxexport class TextComponent extends React.Component { render() { return <h1>Hello, {this.props.name}!</h1>; } } function App() { return ( <div className="main"> <TextComponent name={"Anton"} /> </div> ) }
Lifecycle Methods in Class Components
Lifecycle methods are special methods that allow you to run code at specific points in a component's lifecycle, such as when a component is created, updated, or destroyed.
Common Lifecycle Methods:
- componentDidMount: is called once, after the component is added to the DOM.
- componentDidUpdate: is called each time after the component is updated when props or state is changed.
- componentWillUnmount: is called once, before the component is removed from the DOM.
Let's explore a class component with state lifecycle methods. We'll create a component that fetches blog posts when it is mounted and renders them on the screen:
jsxexport class PostList extends React.Component { constructor(props) { super(props); this.state = { data: [] }; } componentDidMount() { console.log("Component is mounted"); fetch("https://jsonplaceholder.typicode.com/posts") .then(response => response.json()) .then(data => this.setState({ data })); } componentDidUpdate(prevProps, prevState) { if (prevState.data !== this.state.data) { console.log("Data updated:", this.state.data); } } componentWillUnmount() { console.log("Component will unmount"); } render() { if (!this.state.data) { return (<div><p>Loading...</p></div>); } return ( <div> {this.state.data.slice(0, 5).map((post) => <Post key={post.id} postData={post} /> )} </div> ); } }
In componentDidMount
method we fetch a list of blog posts from the test URL and save them into component's state.
componentDidUpdate
method is invoked after the component updates and needs to be re-rendered.
Here it checks if the data state variable has changed. If true, it logs the new data.
This method is called when component's state is populated with blog posts.
componentWillUnmount
method is called just before a component is destroyed or removed from the DOM.
It happens when a user navigates to a different page and the component disappeared from the screen.
Now let's explore how to migrate all mentioned class components into functional components with hooks.
Migrating Classes Without State and Lifecycle To Functional Components
Let's transform our first Hello World
component into a functional one:
jsxconst TextComponent = () => { return <h1>Hello, World!</h1>; }
If the component has only a render logic and doesn't have any additional javascript code, you can omit the { }
and return
:
jsxconst TextComponent = () => ( <h1>Hello, World!</h1>; )
Migrating Classes With State To Functional Components
Now let's migrate the Counter
component into a functional one:
jsxconst Counter = () => { const [count, setCount] = useState(0); const increment = () => { setCount(count + 1); }; return ( <div className="flex"> <p>Count: {count}</p> <button onClick={increment}>Increment</button> </div> ); };
Here we are using the useState
hook that allows to save and update data inside a functional component.
The useState
hook contains a variable itself and a function that allows to a change value of the variable.
Everytime a setCount
function is called - a component is rendered with a new value of count
variable.
As you can see this functional approach with useState
hook requires less code to write and is easy to get used to.
Migrating Classes With Props To Functional Components
Functional components receive props as the function argument and the TextComponent
migration is straightforward:
jsxconst TextComponent = (props) => { return <h1>Hello, {props.name}!</h1>; } function App() { return ( <div className="main"> <TextComponent name={"Anton"} /> </div> ) }
Migrating Classes With Lifecycle Methods To Functional Components
Let's migrate the PostList
component to the functional one:
jsximport React, { useEffect, useState } from 'react'; export const PostList = () => { const [data, setData] = useState([]); useEffect(() => { console.log("Component is mounted"); fetch('https://jsonplaceholder.typicode.com/posts') .then((response) => response.json()) .then((data) => setData(data)); return () => { console.log("Component will unmount"); }; }, []); useEffect(() => { console.log("Data updated:", this.state.data); }, [data]); if (!data.length) { return ( <div> <p>Loading...</p> </div> ); } return ( <div> {data.slice(0, 5).map((post) => ( <Post key={post.id} postData={post} /> ))} </div> ); };
This functional approach also requires less code and doesn't require additional condition checks whether the data is updated.
In class components when you have a lot of data in the state that is changing - the componentDidUpdate
method can become a big ball of mud with a lot of conditional checks.
In functional component the useEffect
hook is used that combines all three class lifecycle functions: componentDidMount
, componentDidUpdate
and componentWillUnmount
.
useEffect
hook has 2 parameters: a function that performs a side effect and an array of tracked variables.
When a variable value changes - a useEffect hook is triggered.
The first hook has an empty braces [ ] without parameters, this effect function will be called only once after a component is loaded.
It is an analog of componentDidMount
class method.
Second useEffect
hook is called only when data variable is changed.
It is an analog of componentDidMount
class method.
You can use as many useEffect
hooks in the functional component as you want.
And you can specify multiple parameters inside [ ] of the hook.
And what about componentWillUnmount
analog? It is represented as a returned function inside a useEffect
without parameters:
jsxuseEffect(() => { console.log("Component is mounted"); // ... return () => { console.log("Component will unmount"); }; }, []);
You can learn more about React hooks in my blog post
Summary
In this blog post we have talked about what React class and functional components are. Both class and functional components have state, props and lifecycle methods.
Components with hook offer a functional approach to writing components. Hooks are a modern way to write your components, and nowadays, most of the React applications are built with them.
So should you jump in and rewrite all class components to the hooks? Not exactly. I recommend writing all new components with hooks if you can update the React version to 16.8.0 in your project. And migrate class components to the functional ones as needed.
Hope you find this blog post useful. Happy coding!