In recent years, there has been an increasingly noticeable trend in software development that prompted developers to transition from object oriented programming (OOP) to functional programming (FP), whenever applicable. React appears to have joined the “movement” and its object oriented class-based React components have been transitioned to the functional components with React Hooks. A similar trend in the Scala world is the ad-hoc polymorphism with type classes as opposed to classic subtype polymorphism.
Given React‘s massive adoption rate and its API being evolved at a breakneck pace, we’re seeing a lot of React applications using a mix of both styles. Inevitably, some old method calls gradually get deprecated in order to align with the design principles of the new Hooks APIs. To maintain backward compatibility while evolving with the new functional approach, wrappers are being used to bundle the React Hooks with the class-based React components.
React Hooks in class-based components
For instance, for a User component to be able to reference the user-id from a HTTP GET query parameter via a React service, the useParams hook can be employed by being wrapped with the User component as a WrappedComponent.
import { connect } from "react-redux";
import { useParams } from "react-router-dom";
import UserDataService from "../services/user.service";
export const withParams = (WrappedComponent) => (props) => {
const params = useParams();
return <WrappedComponent {...props} params={params} />;
};
...
class User extends Component {
...
componentDidMount() {
this.getUser(this.props.params.id);
}
...
getUser(id) {
UserDataService.find(id)
.then(...)
.catch(...);
}
...
render() {
...
return {
// Display UI
...
}
}
}
export default connect()(withParams(User));
A couple of notes:
- While a
Reacthook looks like an ordinary function, it can only be used at the top level (or inside a customReacthook) and cannot be used within theReactcomponent class. - If additional
React Hooksare needed, they’d have to be successively wrapped. Say, if hooksuseNavigateanduseLocationare included, one would need to compose thewithNavigateandwithLocationtop-level functions followed by wrapping them asWrappedComponentlike below:
export default connect()(withLocation(withNavigate(withParams(User))));
React Hooks in functional components
Even though React Hooks are by design for functional components, the limitation of being at the top level (or from within custom hooks) still holds. That’s because the state of the hooks are internally “intertwined” with the React renderer throughout the rendering cycle of the React DOM.
Here’s what a functional equivalence of the above class-based component might look like:
import React, { useEffect } from "react";
import { useParams } from "react-router-dom";
import UserDataService from "../services/user.service";
...
const User = () => {
const params = useParams();
...
useEffect(() => {
getUser(params.id);
}, [params.id]);
const getUser = (id) => {
UserDataService.find(id)
.then(...)
.catch(...);
}
...
return (
// Display UI
...
)
}
export default User;
Comparing a class-based component with a functional component
As almost always the case, the easiest way to see the key differences between class-based and functional React components is to skim through the class-based and functional versions of a typical component with equivalent functionality.
First, a sample snippet of a class-based React component:
import React, { Component } from "react";
import { connect } from "react-redux";
import { Link, useParams } from "react-router-dom";
import { updateUser } from "../actions/user";
import UserDataService from "../services/user.service";
export const withParams = (WrappedComponent) => (props) => {
const params = useParams();
return <WrappedComponent {...props} params={params} />;
};
class User extends Component { // User is a class
constructor(props) {
super(props);
this.onChangeUsername = this.onChangeUsername.bind(this);
this.onChangeEmail = this.onChangeEmail.bind(this);
this.onChangeFirstName = this.onChangeFirstName.bind(this);
this.onChangeLastName = this.onChangeLastName.bind(this);
this.getUser = this.getUser.bind(this);
this.updateContent = this.updateContent.bind(this);
this.state = {
currentUser: {
id: null,
username: "",
email: "",
firstName: "",
lastName: ""
},
message: ""
};
}
componentDidMount() { // Component lifecycle function called after render() is called
this.getUser(this.props.params.id);
}
// onChange functions for each input fields as class methods
onChangeUsername(e) {
const username = e.target.value;
this.setState(function (prevState) {
return {
currentUser: {
...prevState.currentUser,
setState: setState,
},
};
});
}
onChangeEmail(e) {
const email = e.target.value;
this.setState(function (prevState) {
return {
currentUser: {
...prevState.currentUser,
email: email,
},
};
});
}
onChangeFirstName(e) {
const firstName = e.target.value;
this.setState((prevState) => ({
currentUser: {
...prevState.currentUser,
firstName: firstName,
},
}));
}
onChangeLastName(e) {
const lastName = e.target.value;
this.setState((prevState) => ({
currentUser: {
...prevState.currentUser,
lastName: lastName,
},
}));
}
getUser(id) {
UserDataService.find(id)
.then(...) // Logic for handling return from
.catch(...); // service call skipped.
}
updateContent() {
this.props
.updateUser(this.state.currentUser.id, this.state.currentUser)
.then(...) // Logic for handling return from
.catch(...); // service call skipped.
}
render() { // Rendering after constructor() is called
const { currentUser, message } = this.state;
const { loginUser } = this.props;
return (
<div className="list row">
<div className="col-md-9">
{currentUser && (currentUser.id === loginUser.id || loginUser.roles.includes('ROLE_ADMIN')) ? (
<div className="edit-form">
<h4>User</h4>
<form>
<div className="row mt-2">
<label className="col-md-3" htmlFor="id">ID</label>
<input
type="number"
className="col-md-9"
id="id"
value={currentUser.id}
disabled readOnly
/>
</div>
<div className="row mt-2">
<label className="col-md-3" htmlFor="username">Username</label>
<input
type="text"
className="col-md-9"
id="username"
value={currentUser.username}
disabled readOnly
/>
</div>
<div className="row mt-2">
<label className="col-md-3" htmlFor="email">Email</label>
<input
type="text"
className="col-md-9"
id="email"
value={currentUser.email}
onChange={this.onChangeEmail}
/>
</div>
<div className="row mt-2">
<label className="col-md-3" htmlFor="firstName">First Name</label>
<input
type="text"
className="col-md-9"
id="firstName"
value={currentUser.firstName}
onChange={this.onChangeFirstName}
/>
</div>
<div className="row mt-2">
<label className="col-md-3" htmlFor="lastName">Last Name</label>
<input
type="text"
className="col-md-9"
id="lastName"
value={currentUser.lastName}
onChange={this.onChangeLastName}
/>
</div>
</form>
<p className="text-warning">{message}</p>
<button
type="submit"
className="btn btn-warning mt-2 mb-2"
onClick={this.updateContent}
>
Update
</button>
</div>
) : (
<div>
<br />
<p>User access not permitted!</p>
</div>
)}
</div>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
loginUser: state.auth.user
};
};
export default connect(mapStateToProps, { updateUser })(withParams(User));
// Using connect() for state info in Redux store
Next, an example of a functional style React component with equivalent functionality:
import React, { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link, useParams } from "react-router-dom";
import { updateUser } from "../actions/user";
import UserDataService from "../services/user.service";
const User = () => {
const params = useParams();
const dispatch = useDispatch();
const loginUser = useSelector(state => state.auth.user);
const initUserState = {
id: params.id,
username: "",
email: "",
firstName: "",
lastName: ""
};
const [currentUser, setCurrentUser] = useState(initUserState);
const [message, setMessage] = useState("");
const handleUserChange = event => {
const { id, value } = event.target;
setCurrentUser({ ...currentUser, [id]: value });
};
useEffect(() => {
getUser(params.id);
}, [params.id]);
const getUser = (id) => {
UserDataService.find(id)
.then(...) // Logic for handling return from
.catch(...); // service call skipped.
};
const updateContent = () => {
dispatch(updateUser(currentUser.id, currentUser))
.then(...) // Logic for handling return from
.catch(...); // service call skipped.
};
return (
<div className="list row">
<div className="col-md-9">
{currentUser && (currentUser.id === loginUser.id || loginUser.roles.includes('ROLE_ADMIN')) ? (
<div className="edit-form">
<h4>User</h4>
<form>
<div className="row mt-2">
<label className="col-md-3" htmlFor="id">ID</label>
<input
type="number"
className="col-md-9"
id="id"
value={currentUser.id}
disabled readOnly
/>
</div>
<div className="row mt-2">
<label className="col-md-3" htmlFor="username">Username</label>
<input
type="text"
className="col-md-9"
id="username"
value={currentUser.username}
disabled readOnly
/>
</div>
<div className="row mt-2">
<label className="col-md-3" htmlFor="email">Email</label>
<input
type="text"
className="col-md-9"
id="email"
value={currentUser.email}
onChange={handleUserChange}
/>
</div>
<div className="row mt-2">
<label className="col-md-3" htmlFor="firstName">First Name</label>
<input
type="text"
className="col-md-9"
id="firstName"
value={currentUser.firstName}
onChange={handleUserChange}
/>
</div>
<div className="row mt-2">
<label className="col-md-3" htmlFor="lastName">Last Name</label>
<input
type="text"
className="col-md-9"
id="lastName"
value={currentUser.lastName}
onChange={handleUserChange}
/>
</div>
</form>
<p className="text-warning">{message}</p>
<button
type="submit"
className="btn btn-warning mt-2 mb-2"
onClick={updateContent}
>
Update
</button>
</div>
) : (
<div>
<br />
<p>User access not permitted!</p>
</div>
)}
</div>
</div>
);
};
export default User;
Differences between class-based and functional React components
Summarizing the key differences:
- Using of
React Hooks– As highlighted earlier in this post, for the class-based component, theReacthook needs to be wrapped as a WrappedComponent class, whereas it’s being used as a top-level function in a functional component. - Class methods with binding vs regular functions – In the class-based component, each class method requires binding (i.e.
.bind(this)) within the constructor so as to make keywordthisrefer to the component context, allowing the method to access component attributes such asthis.props,this.state. On the other hand, functions for UI events in the functional component are no different from regular functions. - State changes – State changes within a class-based component are typically initialized and maintained as class fields each with its corresponding event handler as a class method. In a functional component, state changes are managed via the
useStateReacthook. - Event handling – For the class-based component, in addition to binding, each input field needs to have its own
onChangeevent handler (e.g. class methodonChangeUsername), whereas for the functional component, it could be much less verbose by generalizing individual event handers into a singlehandleUserChangefunction as long as theonChangeevent handling logics for the input fields are similar. - Rendering lifecycle – A class-based component has a lifecycle of
constructor → render() → componentDidMount(), in that order. For a functional component, various kinds ofReact hookshave their specific “hooks” associated with the component’s lifecycle, whereas ordinary function calls within the main component are handled in programmatic order - Managing state in
React Reduxstore – Class-based components rely on the connect API along with specialty functions such as mapStateToProps to be called upon state changes in the Redux store to extract state info into the component. As for functional components, certainReact HooksincludinguseSelectorcan be used for fetching state data fromReduxstore, as shown in the previous blog post.
Final thoughts
Based on what we’ve gone over, the functional approach to creating a React component obviously offers some advantages over the class-based approach. Code readability due to the minimizing of boilerplate code and consistency of treating event handlers as functions is perhaps the biggest plus.
Looking at the sample source code, the difference between class-based and functional components might seem drastic. But once we’ve had some fundamental understanding of how a React component handles UI events and maintains states throughout its lifecycle, the difference would become self-explanatory.
Nonetheless, migrating a large amount of class-based React components would still require significant investment in coding as well as testing. Since there is no sign support of class-based components will end any time soon, I’d say leaving the already proven working code as a mid-to-low priority tech debt is reasonable.
