In the modern web developer world, there is no shortage of React files written as old class components, and so we need to know how to convert them to functional components when needed.
Basic Refactoring Steps
State Management: In a class component, you use this.state
and this.setState
. In a functional component, you'll use useState
or Redux state hooks useAppSelector
.
Lifecycle Methods: Class components use lifecycle methods like componentDidMount
or componentDidUpdate
. In a functional component, you can replicate these using useEffect
.
Dispatch Actions: In a class component, you use this.props.dispatch
and get dispatch
from prop like DispatchProp
. In a functional component, you'll use useDispatch
from react-redux
for dispatching actions. Also in class component, you usually use mapDispatchToProps
function to conveniently map dispatch action to props of the component for ease of use (even though you can directly call this.props.dispatch
). In a functional component, these is no longer the need to use mapDispatchToProps
because the useDispatch
hook is much easier to setup and use.
Redux State: In a class component, you use mapStateToProps
and connect the component to Redux with export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)
. This isn't necessary anymore, with functional component, to use Redux State, we can use useAppSelector
and it will both get the state and connect the component with Redux.
Example: Class to Function Component Conversion
Let’s assume you have a class component like this:
Class Component
import React from 'react';
import { connect, DispatchProp } from 'react-redux';
import { fetchData } from './actions';
import { calculateWidth } from './utility';
import { AppState } from './types';
import { changeLocale, changeLastModifiedDate } from './modules'
import { Dispatch } from 'redux'
type Props = {
money: number;
fetchData: (id: number) => void;
changeLocale: (locale: string) => void;
} & DispatchProp // This allow you to use this.props.dispatch
& ReturnType<typeof mapStateToProps> // This combine type of mapStateToProps function and your props
type States = {
currentMoney: number;
loading: boolean;
isHighRank: boolean;
lastModified: Date;
}
class MyComponent extends React.Component<Props, States> {
constructor(props: Props) {
super(props);
this.state = {
currentMoney: props.money,
loading: false,
isHighRank: false,
lastModified: new Date(),
};
}
static getDerivedStateFromProps(props, states) {
if (props.money !== states.currentMoney) {
return {
currentMoney: props.money,
};
}
return null;
}
componentDidMount() {
this.setState({ loading: true });
this.props.fetchData(this.props.user.id);
}
componentWillUnmount() {
this.props.dispatch(changeLastModifiedDate(this.state.lastModified))
}
componentDidUpdate(prevProps: Props) {
if (prevProps.data !== this.props.data) {
this.setState({ loading: false });
}
}
render() {
const { data } = this.props;
const { loading } = this.state;
const width = calculateWidth(this.props.user.resolution)
return (
<>
<div style={{ width: width }}>
{loading ? <p>Loading...</p> : <ul>{data.map(item => <li key={item}>{item}</li>)}</ul>}
</div>
<div>{this.state.currentMoney}</div>
</>
);
}
}
const mapStateToProps = (state: AppState) => ({
data: state.data,
user: state.user
});
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
changeLocale: (locale: string) => dispatch(changeLocale(locale)),
fetchData: (id: number) => dispatch(fetchData(id))
}
};
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);
Converted to Functional Component
Here’s how you would convert the above class component to a functional component with hooks (useState
, useEffect
) and Redux (useDispatch
, useAppSelector
).
Functional Component
import React, { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux'
import { fetchData } from './actions';
import { calculateWidth } from './utility';
import { AppState } from './types';
import { changeLocale, changeLastModifiedDate } from './modules';
import { useAppSelector } from 'stores/index'
type Props = {
money: number;
}
// Remove DispatchProp because we can use useDispatch() to get dispatch from Redux
// Remove return type of mapStateToProps function because we can use useAppSelector to get Redux state
// Remove type States because we can define multiple state with useState
// You can still use type States if you refer creating one big state object with useState
const MyComponent: React.FC<Props> = ({ money }) => {
// Get dispatch directly without using DispatchProp and connect function
const dispatch = useDispatch()
// Remove constructor and the need of setting this.state to a default object because now we can use multiple useState
const [loading, setLoading] = useState(false);
const [isHighRank, setIsHighRank] = useState(false);
const [lastModified, setLastModified] = useState<Date>(new Date());
// Initialize state with the money prop
const [currentMoney, setCurrentMoney] = useState(money);
// Get Redux state directly without the need of mapStateToProps and connect function
const data = useAppSelector((state: AppState) => state.data)
const user = useAppSelector((state: AppState) => state.user)
let width = 0
// useEffect to update currentMoney state when the money prop changes, which replacing getDerivedStateFromProps
useEffect(() => {
if (money !== currentMoney) {
setCurrentMoney(money); // Update currentMoney if prop changes
}
}, [money, currentMoney]); // If dependency change, useEffect will re-run
// useEffect can be componentDidMount, componentWillUnmount, componentDidUpdate all in one package
useEffect(() => {
// Every useEffect run once when the component is rendered, so it is equivalent to componentDidMount
setLoading(true);
dispatch(fetchData(user.id));
// return inside useEffect replace componentWillUnmount
return () => {
dispatch(changeLastModifiedDate(lastModified));
};
}, [fetchData, user.id, dispatch, lastModified]); // If dependency change, useEffect will re-run
// This useEffect is equivalent to componentDidUpdate since it will run if data is changed
useEffect(() => {
if (data) {
setLoading(false);
}
}, [data]); // If dependency change, useEffect will re-run
// You could write const width = calculateWidth(user.resolution); here
// if the implementation isn't cause side effect like updating state or an async call to fetch API
// However in most case, the logic inside render() function should be converted to an useEffect
useEffect(() => {
width = calculateWidth(user.resolution);
}, [user.resolution]); // If dependency change, useEffect will re-run
// Replace render() function with an return
return (
<>
<div style={{ width: width }}>
{loading ? <p>Loading...</p> : <ul>{data.map(item => <li key={item}>{item}</li>)}</ul>}
</div>
<div>{money}</div>
</>
);
};
// Remove mapStateToProps, mapDispatchToProps
// Remove the connect
export default MyComponent;
Thank you for reading tutorial. I hope this post will help you understand better the way of converting class component into functional component.