In this article, we will build an application using class components and then later convert it to functional components using React Hooks in a step-by-step way.
By building this app, you will learn:
- How to make API calls
- How to implement load more functionality
- How to debug application issues
- How to use async/await
- How to update the component when something changes
- How to fix the infinite loop issue in the useEffect hook
- How to refactor class-based components into functional components with Hooks
and much more.
So let’s get started.
Want to learn Redux from the absolute beginning and build a food ordering app from scratch? Check out the Mastering Redux course.
Initial Project Setup
Create a new project using create-react-app
:
npx create-react-app class-to-hooks-refactoring
Once the project is created, delete all files from the src
folder and create the index.js
file and the styles.css
file inside the src
folder. Also, create components
folders inside the src
folder.
Install the axios
library by executing the following command from the project folder:
yarn add axios@0.21.1
Open styles.css
file and add the contents from here inside it.
How to Create the Initial Pages
Create a new file Header.js
inside the components
folder with the following content:
import React from "react";
const Header = () => {
return <h1 className="header">Random Users</h1>;
};
export default Header;
Create a new file App.js
inside the src
folder with the following content:
import React from 'react';
import Header from './components/Header';
export default class App extends React.Component {
render() {
return (
<div className="main-section">
<Header />
<h2>App Component</h2>
</div>
);
}
}
Now, open index.js
file and add the following contents into it:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './styles.css';
ReactDOM.render(<App />, document.getElementById('root'));
Now, start the application by running the yarn start
command from the terminal.
You will see the following screen If you access the application at http://localhost:3000/.
How to Make an API Call
We will be using Random Users API to get a list of random users.
So open App.js
file and add componentDidMount
method inside the component:
componentDidMount() {
axios
.get('https://randomuser.me/api/?page=0&results=10')
.then((response) => {
console.log(response.data);
})
.catch((error) => console.log('error', error));
}
Also, import axios
at the top of the file:
import axios from 'axios';
Your entire App.js
file will look like this now:
import React from 'react';
import Header from './components/Header';
import axios from 'axios';
export default class App extends React.Component {
componentDidMount() {
axios
.get('https://randomuser.me/api/?page=0&results=10')
.then((response) => {
console.log(response.data);
})
.catch((error) => console.log('error', error));
}
render() {
return (
<div className="main-section">
<Header />
<h2>App Component</h2>
</div>
);
}
}
Here, we're making an API call to get a list of 10 records initially to the URL https://randomuser.me/api/?page=0&results=10
.
Now, If you check the application, you will see the response from the API in the console.
Now, let's declare a state to store the result and flags related to the loading and error message.
Replace the contents of App.js
file with the following code:
import React from 'react';
import Header from './components/Header';
import axios from 'axios';
export default class App extends React.Component {
state = {
users: [],
isLoading: false,
errorMsg: ''
};
componentDidMount() {
this.setState({ isLoading: true });
axios
.get('https://randomuser.me/api/?page=0&results=10')
.then((response) => {
this.setState({ users: response.data.results, errorMsg: '' });
})
.catch((error) =>
this.setState({
errorMsg: 'Error while loading data. Try again later.'
})
)
.finally(() => {
this.setState({ isLoading: false });
});
}
render() {
const { users, isLoading, errorMsg } = this.state;
console.log(users);
return (
<div className="main-section">
<Header />
{isLoading && <p className="loading">Loading...</p>}
{errorMsg && <p className="errorMsg">{errorMsg}</p>}
</div>
);
}
}
Here, we've declared a state directly inside the class using class properties syntax which is a common way to write state in class based components.
state = {
users: [],
isLoading: false,
errorMsg: ''
};
Then inside the componentDidMount
method, we're first setting the isLoading
state to true
before making the API call.
this.setState({ isLoading: true });
Once we got the API response, we're storing the result in the users
array which is declared in the state and setting the errorMsg
state to empty so If there is any previous error, it will be cleared out.
this.setState({ users: response.data.results, errorMsg: '' });
And in the .catch
block, we're setting the errorMsg
If there is any error while making an API call.
Then, we're using the .finally
block to set the isLoading
state to false
.
.finally(() => {
this.setState({ isLoading: false });
});
Using finally
helps to avoid code duplication here because we don't need to set isLoading
to false
in .then
and in .catch
block again as finally
block will always be executed even If there is success or error.
And in the render method, we're displaying either the error message or loading message along with the users
array from the state in the console.
Now, If you check the application, you will see users
information in the console on success or an error message on the UI for API failure.
How to Display the Users Information
Now, let's display the users
information on the screen.
Create a new file User.js
inside the components
folder with the following content:
import React from "react";
const User = ({ name, location, email, picture }) => {
return (
<div className="random-user">
<div className="user-image">
<img src={picture.medium} alt={name.first} />
</div>
<div className="user-details">
<div>
<strong>Name:</strong> {name.first} {name.last}
</div>
<div>
<strong>Country:</strong> {location.country}
</div>
<div>
<strong>Email:</strong> {email}
</div>
</div>
</div>
);
};
export default User;
Now, create a new file UsersList.js
inside the components
folder with the following content:
import React from 'react';
import User from './User';
const UsersList = ({ users }) => {
return (
<div className="user-list">
{users && users.map((user) => <User key={user.login.uuid} {...user} />)}
</div>
);
};
export default UsersList;
Now, open App.js
file and replace the render
method with the following code:
render() {
const { users, isLoading, errorMsg } = this.state;
return (
<div className="main-section">
<Header />
{isLoading && <p className="loading">Loading...</p>}
{errorMsg && <p className="errorMsg">{errorMsg}</p>}
<UsersList users={users} />
</div>
);
}
Here, we're passing the users
array as a prop to the UsersList
component and inside the UsersList
component, we're looping over the array and sending the user information to the User
component by spreading out all the properties of the individual user
as {...props}
which finally displays the data on the screen.
Also, import the UsersList
component at the top of the file:
import UsersList from './components/UsersList';
If you check the application now, you will see the following screen:
As you can see, on every page refresh, a new set of random users are displayed on the screen.
How to Add the Load More Functionality
Now, let's add load more functionality to load the next set of 10 users on every load more click.
Change the render
method of the App.js
file to the following code:
render() {
const { users, isLoading, errorMsg } = this.state;
return (
<div className="main-section">
<Header />
<UsersList users={users} />
{errorMsg && <p className="errorMsg">{errorMsg}</p>}
<div className="load-more">
<button onClick={this.loadMore} className="btn-grad">
{isLoading ? 'Loading...' : 'Load More'}
</button>
</div>
</div>
);
}
Here, we've added the isLoading
check inside the button to display either Loading...
or Load More
text on the button.
Add a new page
property to the state and initialize it to 0
.
state = {
users: [],
page: 0,
isLoading: false,
errorMsg: ''
};
And add the loadMore
handler function before the render
method to increment the page
state value by 1 on every button click.
loadMore = () => {
this.setState((prevState) => ({
page: prevState.page + 1
}));
};
Here, we're using previous state to calculate the next state value of page so the above code is the same as below code:
loadMore = () => {
this.setState((prevState) => {
return {
page: prevState.page + 1
};
});
};
We're just using ES6 shorthand syntax for returning an object from the function.
Now, Inside the componentDidMount
method, change the API URL from the below code:
'https://randomuser.me/api/?page=0&results=10'
to this code:
`https://randomuser.me/api/?page=${page}&results=10`
Here, we're using the ES6 template literal syntax to use the dynamic value of the page
state to load the next set of users on every button click.
Destructure the page
from state inside the componentDidMount
method like this:
componentDidMount() {
const { page } = this.state;
....
}
Want to explore all the ES6+ features in detail? Check out my Mastering Modern JavaScript book.
Now, let's check the application functionality.
As you can see, when we click on the Load More
button, the page
state is changing in the react dev tools but we're not getting the new list of users displayed on the screen.
This is because even though we're changing the page
state, we're not making API call again to get the next set of users with the changed page
value. So let's fix this.
Create a new loadUsers
function above the loadMore
function and move all the code from componentDidMount
to inside the loadUsers
function and call the the loadUsers
function from the componentDidMount
method.
Also, add a componentDidUpdate
method inside the App
component like this:
componentDidUpdate(prevProps, prevState) {
if (prevState.page !== this.state.page) {
this.loadUsers();
}
}
As we're updating the value of the page
state in loadMore
function once the state is updated, the componentDidUpdate
method will be called so we're checking If the previous state value of page
is not equal to the current state value and then make the API call again by calling the loadUsers
function.
Check out my previous article to learn more about why and when we need to use the
componentDidUpdate
method.
Your complete App.js
file will look like this now:
import React from 'react';
import Header from './components/Header';
import axios from 'axios';
import UsersList from './components/UsersList';
export default class App extends React.Component {
state = {
users: [],
page: 0,
isLoading: false,
errorMsg: ''
};
componentDidMount() {
this.loadUsers();
}
componentDidUpdate(prevProps, prevState) {
if (prevState.page !== this.state.page) {
this.loadUsers();
}
}
loadUsers = () => {
const { page } = this.state;
this.setState({ isLoading: true });
axios
.get(`https://randomuser.me/api/?page=${page}&results=10`)
.then((response) => {
this.setState({ users: response.data.results, errorMsg: '' });
})
.catch((error) =>
this.setState({
errorMsg: 'Error while loading data. Try again later.'
})
)
.finally(() => {
this.setState({ isLoading: false });
});
};
loadMore = () => {
this.setState((prevState) => ({
page: prevState.page + 1
}));
};
render() {
const { users, isLoading, errorMsg } = this.state;
return (
<div className="main-section">
<Header />
<UsersList users={users} />
{errorMsg && <p className="errorMsg">{errorMsg}</p>}
<div className="load-more">
<button onClick={this.loadMore} className="btn-grad">
{isLoading ? 'Loading...' : 'Load More'}
</button>
</div>
</div>
);
}
}
Now, If you check the application again by running the yarn start
command you will see the following screen:
As you can see, we're getting a new list of users displayed on every load more button click. But the issue is that we're able to see only 10 users at a time.
So let's make changes to add new users to the already displayed list of users.
For this, we need to make changes in the way we're setting the users
state.
Our current setState
call inside the loadUsers
function looks like this:
this.setState({ users: response.data.results, errorMsg: '' });
Here, we're always replacing the users
array with the new set of users. So change the above setState
call to the following code:
this.setState((prevState) => ({
users: [...prevState.users, ...response.data.results],
errorMsg: ''
}));
Here, we're using updater syntax of setState
where we're creating a new array by spreading out the already added users
by using ...prevState.users
and then adding a new set of users
by using ...response.data.results
.
So this way we'll not lose the previous loaded users
data and also able to append a new set of users
.
Now, If you check the application again, you will see the correct behavior of data loading.
How to Improve the Code using Async/await
If you check the loadUsers
function, you will see that the code looks complex and difficult to read at once.
loadUsers = () => {
const { page } = this.state;
this.setState({ isLoading: true });
axios
.get(`https://randomuser.me/api/?page=${page}&results=10`)
.then((response) => {
this.setState((prevState) => ({
users: [...prevState.users, ...response.data.results],
errorMsg: ''
}));
})
.catch((error) =>
this.setState({
errorMsg: 'Error while loading data. Try again later.'
})
)
.finally(() => {
this.setState({ isLoading: false });
});
};
We can fix this using async/await syntax.
First, we need to mark the loadUsers
function as async:
loadUsers = async () => {
Because we can use the await
keyword only inside the function which is declared as async
.
Now, replace the loadUsers
function with the following code:
loadUsers = async () => {
try {
const { page } = this.state;
this.setState({ isLoading: true });
const response = await axios.get(
`https://randomuser.me/api/?page=${page}&results=10`
);
this.setState((prevState) => ({
users: [...prevState.users, ...response.data.results],
errorMsg: ''
}));
} catch (error) {
this.setState({
errorMsg: 'Error while loading data. Try again later.'
});
} finally {
this.setState({ isLoading: false });
}
};
Here, we've used the await
keyword before the axios.get
call so the next line of code which is the setState
call will not be executed until we get the response from the API.
If there is any error while getting the response from API, the catch
block will be executed and finally
block will set the isLoading
state to false
.
Your changed App.js
file will look like this now:
import React from 'react';
import Header from './components/Header';
import axios from 'axios';
import UsersList from './components/UsersList';
export default class App extends React.Component {
state = {
users: [],
page: 0,
isLoading: false,
errorMsg: ''
};
componentDidMount() {
this.loadUsers();
}
componentDidUpdate(prevProps, prevState) {
if (prevState.page !== this.state.page) {
this.loadUsers();
}
}
loadUsers = async () => {
try {
const { page } = this.state;
this.setState({ isLoading: true });
const response = await axios.get(
`https://randomuser.me/api/?page=${page}&results=10`
);
this.setState((prevState) => ({
users: [...prevState.users, ...response.data.results],
errorMsg: ''
}));
} catch (error) {
this.setState({
errorMsg: 'Error while loading data. Try again later.'
});
} finally {
this.setState({ isLoading: false });
}
};
loadMore = () => {
this.setState((prevState) => ({
page: prevState.page + 1
}));
};
render() {
const { users, isLoading, errorMsg } = this.state;
return (
<div className="main-section">
<Header />
<UsersList users={users} />
{errorMsg && <p className="errorMsg">{errorMsg}</p>}
<div className="load-more">
<button onClick={this.loadMore} className="btn-grad">
{isLoading ? 'Loading...' : 'Load More'}
</button>
</div>
</div>
);
}
}
Now, the loadUsers
function code looks much cleaner and easy to understand than the previous one and If you check the application, you will see that the application is also working correctly.
How to Refactor Class Component Code to Functional Component Code
Now, we're done with the complete functionality of the app, let's refactor the code to Functional components with Hooks.
If you're new to React Hooks, check out my this article for an introduction to React Hooks.
Create a new file AppFunctional.js
inside the src
folder with the following content:
import React from 'react';
const AppFunctional = () => {
return (
<div>
<h2>Functional Component</h2>
</div>
);
};
export default AppFunctional;
We've created a new file for the functional component so you will be able to compare both the code and keep it for your reference.
Now, open index.js
file and replace the contents of the file with the following code:
import React from 'react';
import ReactDOM from 'react-dom';
import AppFunctional from './AppFunctional';
import './styles.css';
ReactDOM.render(<AppFunctional />, document.getElementById('root'));
Here, we've used the AppFunctional
component inside the render
method and also added the import for the same at the top of the file.
Now, If you restart your application using the yarn start
command you will see the following screen:
So we're correctly displaying the AppFunctional
component code on the screen.
Now, replace the contents of theAppFunctional
component with the following code:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import Header from './components/Header';
import UsersList from './components/UsersList';
const AppFunctional = () => {
const [users, setUsers] = useState([]);
const [page, setPage] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [errorMsg, setErrorMsg] = useState('');
useEffect(() => {
const loadUsers = async () => {
try {
setIsLoading(true);
const response = await axios.get(
`https://randomuser.me/api/?page=${page}&results=10`
);
setUsers([...users, ...response.data.results]);
setErrorMsg('');
} catch (error) {
setErrorMsg('Error while loading data. Try again later.');
} finally {
setIsLoading(false);
}
};
loadUsers();
}, []);
const loadMore = () => {
setPage((page) => page + 1);
};
return (
<div className="main-section">
<Header />
<UsersList users={users} />
{errorMsg && <p className="errorMsg">{errorMsg}</p>}
<div className="load-more">
<button onClick={loadMore} className="btn-grad">
{isLoading ? 'Loading...' : 'Load More'}
</button>
</div>
</div>
);
};
export default AppFunctional;
Here, we've initially declared the required states using the useState
hook:
const [users, setUsers] = useState([]);
const [page, setPage] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [errorMsg, setErrorMsg] = useState('');
Then we've added a useEffect
hook and passed an empty array []
as the second argument to it so the code inside the useEffect
hook will be executed only once when the component is mounted.
useEffect(() => {
// your code
}, []);
We've moved the entire loadUsers
function inside the useEffect
hook and then called it inside the hook like this:
useEffect(() => {
const loadUsers = async () => {
// your code
};
loadUsers();
}, []);
We've also removed all the references to this.state
as functional components don't need this
context.
Before making the API call, we're setting the isLoading
state to true
using setIsLoading(true);
.
As we already have access to the users
array inside the component, we're directly setting as new array for the setUsers
function like this:
setUsers([...users, ...response.data.results]);
If you want to know why we can't use the
async
keyword directly for theuseEffect
hook function, check out my this article.
Then we've changed the loadMore
function from the below code:
loadMore = () => {
this.setState((prevState) => ({
page: prevState.page + 1
}));
};
to this code:
const loadMore = () => {
setPage((page) => page + 1);
};
Note that, to declare a function in functional components you need to add
const
orlet
before the declaration. As the function is not going to change, it's recommended to useconst
such asconst loadMore = () => { }
.
Then we've copied the render
method content as it is inside the AppFunctional
component for returning the JSX and changed onClick={this.loadMore}
to onClick={loadMore}
.
return (
<div className="main-section">
<Header />
<UsersList users={users} />
{errorMsg && <p className="errorMsg">{errorMsg}</p>}
<div className="load-more">
<button onClick={loadMore} className="btn-grad">
{isLoading ? 'Loading...' : 'Load More'}
</button>
</div>
</div>
);
Now, If you check the application, you will see the following screen:
As you can see, the users are correctly getting loaded but the load more functionality does not work.
This is because we're only making the API call once when the component is mounted as we're passing the empty dependency array []
as the second argument to the useEffect
hook.
To make the API call again when the page
state changes, we need to add the page
as a dependency for the useEffect
hook like this:
useEffect(() => {
// execute the code to load users
}, [page]);
The above useEffect
is the same as writing the below code:
componentDidUpdate(prevProps, prevState) {
if (prevState.page !== this.state.page) {
// execute the code to load users
}
}
The useEffect
makes it really easy to write less code that is easy to understand.
So now with this change, the code inside the useEffect
hook will be executed when the component mounts as well as when the page
state is changed.
Now, If you check the application, you will see that the load more functionality is working again as expected.
But If you check the terminal/command prompt, you might see a warning as shown below (If you've ESLint
installed on your machine):
The warnings help us to avoid issues in our application that might occur later so it's always good to fix those warnings If possible.
As we're referencing the users
state inside the loadUsers
function, we need to include that also in the dependency array. So let's do that.
Include the users
as dependency along with the page
like this:
useEffect(() => {
// your code
}, [page, users]);
Let's check the application functionality now.
As you can see, we're continuously getting a new set of users as we scroll the page and the application is going in an infinite loop.
This is because, when the component is mounted, the code inside the useEffect
hook will be executed to make an API call and once we get the result, we're setting the users
array and as users
is mentioned in the dependencies list, once the users
array is changed, the useEffect
will run again and it will happen again and again creating an infinite loop.
So to fix this, we need to avoid referencing the external users
array somehow. So let's use the updater syntax of set state to set the users
state.
Therefore, change the below code:
setUsers([...users, ...response.data.results]);
to this code:
setUsers((users) => [...users, ...response.data.results]);
Here, we're using the previous value of users to create a new users
array.
Now, we can remove the users
from the useEffect
dependencies array as we're not referencing the external users
variable.
Your changed useEffect
hook will look like this now:
useEffect(() => {
const loadUsers = async () => {
try {
setIsLoading(true);
const response = await axios.get(
`https://randomuser.me/api/?page=${page}&results=10`
);
setUsers((users) => [...users, ...response.data.results]);
setErrorMsg('');
} catch (error) {
setErrorMsg('Error while loading data. Try again later.');
} finally {
setIsLoading(false);
}
};
loadUsers();
}, [page]);
If you check the application now, you will see that the application is working as expected without any issue.
and we're also not getting any error in the terminal now.
Thanks for reading!
You can find the complete source code for this application in this repository and a live demo of the deployed application here.
Starting with ES6, there are many useful additions to JavaScript like:
- ES6 Destructuring
- Import and Export Syntax
- Arrow functions
- Promises
- Async/await
- Optional chaining operator and a lot more.
You can learn everything about all the ES6+ features in detail in my Mastering Modern JavaScript book.
Check out free preview contents of the book here.
Also, you can check out my free Introduction to React Router course to learn React Router from scratch.
Want to stay up to date with regular content regarding JavaScript, React, Node.js? Follow me on LinkedIn.