Refactor React Class Component to Functional Component with Redux and TypeScript

Refactor React Class Component to Functional Component with Redux and TypeScript

SPARKLE

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.