Lab 18: HTTP GET
Objectives
- Create an API object that loads data from an REST API
- Update a component to use the API object
- Add Pagination
Steps
Create an API object that loads data from an REST API
Create the file
src\projects\projectAPI.js.Create a
projectAPIobject and export it from the file.Implement a
getmethod that requirespageandlimitparameters and sets the default topage = 1andlimit=20. The projects should be sorted by name.json-server supports sorting and paging using the following syntax.
`${url}?_page=${page}&_limit=${limit}&_sort=name`;src\projects\projectAPI.jsimport { Project } from './Project';
const baseUrl = 'http://localhost:4000';
const url = `${baseUrl}/projects`;
function translateStatusToErrorMessage(status) {
switch (status) {
case 401:
return 'Please login again.';
case 403:
return 'You do not have permission to view the project(s).';
default:
return 'There was an error retrieving the project(s). Please try again.';
}
}
function checkStatus(response) {
if (response.ok) {
return response;
} else {
const httpErrorInfo = {
status: response.status,
statusText: response.statusText,
url: response.url,
};
console.log(`log server http error: ${JSON.stringify(httpErrorInfo)}`);
let errorMessage = translateStatusToErrorMessage(httpErrorInfo.status);
throw new Error(errorMessage);
}
}
function parseJSON(response) {
return response.json();
}
// eslint-disable-next-line
function delay(ms) {
return function (x) {
return new Promise((resolve) => setTimeout(() => resolve(x), ms));
};
}
const projectAPI = {
get(page = 1, limit = 20) {
return fetch(`${url}?_page=${page}&_limit=${limit}&_sort=name`)
.then(delay(600))
.then(checkStatus)
.then(parseJSON)
.then((projects) => {
return projects.map((p) => {
return new Project(p);
});
})
.catch((error) => {
console.log('log client error ' + error);
throw new Error(
'There was an error retrieving the projects. Please try again.'
);
});
},
};
export { projectAPI };
Update a component to use the API object
Open the file
src\projects\ProjectsPage.jsx.Use the
useStatefunction to create two additional state variablesloadinganderror.src\projects\ProjectsPage.jsx...
function ProjectsPage() {
const [projects, setProjects] = useState(MOCK_PROJECTS);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(undefined);
...
}DO NOT DELETE the file
src\projects\MockProjects.js. We will use it in our unit testing.Change the
projectsstate to be an empty array[](be sure to remove the mock data).src\projects\ProjectsPage.jsx- import { MOCK_PROJECTS } from './MockProjects';
...
function ProjectsPage() {
- const [projects, setProjects] = useState(MOCK_PROJECTS);
+ const [projects, setProjects] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(undefined);
...
}
...Implement the loading of the data from the API after the initial component render in a
useEffecthook. Follow these specifications.- Set state of
loadingtotrue - Call the API:
projectAPI.get(1). - If successful, set the returned
datainto the componentsprojectsstate variable and set theloadingstate variable tofalse. - If an error occurs, set the returned error's message
error.messageto the componentserrorstate andloadingtofalse.
src\projects\ProjectsPage.jsx- Set state of
import { useState,
+ useEffect } from 'react';
+ import { projectAPI } from './projectAPI';
function ProjectsPage() {
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(undefined);
// Approach 1: using promise then
// useEffect(() => {
// setLoading(true);
// projectAPI
// .get(1)
// .then((data) => {
// setError(null);
// setLoading(false);
// setProjects(data);
// })
// .catch((e) => {
// setLoading(false);
// setError(e.message);
// });
// }, []);
// Approach 2: using async/await
+ useEffect(() => {
+ async function loadProjects() {
+ setLoading(true);
+ try {
+ const data = await projectAPI.get(1);
+ setError(null);
+ setProjects(data);
+ } catch (e) {
+ setError(e.message);
+ } finally {
+ setLoading(false);
+ }
+ }
+ loadProjects();
+ }, []);
...
}
...
Display the loading indicator below the
<ProjectList />. Only display the indicator whenloading=true.If you want to try it yourself first before looking at the solution code use the
HTMLsnippet below to format the loading indicator.<div class="center-page">
<span class="spinner primary"></span>
<p>Loading...</p>
</div>src\projects\ProjectsPage.jsxfunction ProjectsPage() {
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(undefined);
...
return (
<>
<h1>Projects</h1>
<ProjectList onSave={saveProject} projects={projects} />
+ {loading && (
+ <div className="center-page">
+ <span className="spinner primary"></span>
+ <p>Loading...</p>
+ </div>
+ )}
</>
);
}
export default ProjectsPage;Add these
CSSstyles to center the loading indicator on the page.src\index.css... //add below existing styles
html,
body,
#root,
.container,
.center-page {
height: 100%;
}
.center-page {
display: flex;
justify-content: center;
align-items: center;
}Display the error message above the
<ProjectList />using theHTMLsnippet below. Only display the indicator whenerroris defined.If you want to try it yourself first before looking at the solution code use the
HTMLsnippet below to format the error.<div class="row">
<div class="card large error">
<section>
<p>
<span class="icon-alert inverse "></span>
{error}
</p>
</section>
</div>
</div>src\projects\ProjectsPage.jsxfunction ProjectsPage() {
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(undefined);
...
return (
<>
<h1>Projects</h1>
+ {error && (
+ <div className="row">
+ <div className="card large error">
+ <section>
+ <p>
+ <span className="icon-alert inverse "></span>
+ {error}
+ </p>
+ </section>
+ </div>
+ </div>
+ )}
<ProjectList onSave={saveProject} projects={projects} />
{loading && (
<div className="center-page">
<span className="spinner primary"></span>
<p>Loading...</p>
</div>
)}
</>
);
}
export default ProjectsPage;Verify the application is working by following these steps in your
Chromebrowser.Open the application on
http://localhost:5173/.Open
Chrome DevTools.Refresh the page.
For a brief second, a loading indicator should appear.

Then, a list of projects should appear.
Click on the
Chrome DevToolsNetworktab.Verify the request to
/projects?_page=1&_limit=20&_sort=nameis happening.
We are using a
delayfunction inprojectAPI.get()to delay the returning of data so it is easier to see the loading indicator. You can remove thedelayat this point.src\projects\projectAPI.jsreturn fetch(`${url}?_page=${page}&_limit=${limit}&_sort=name`)
- .then(delay(600))
.then(checkStatus)
.then(parseJSON);Change the URL so the API endpoint cannot be reached.
src\projects\projectAPI.jsconst baseUrl = 'http://localhost:4000';
- const url = `${baseUrl}/projects`;
+ const url = `${baseUrl}/fail`;
...In your browser, you should see the following error message displayed.

Fix the URL to the backend API before continuing to the next lab.
src\projects\projectAPI.js...
const baseUrl = 'http://localhost:4000';
+ const url = `${baseUrl}/projects`;
- const url = `${baseUrl}/fail`;
...
Add Pagination
Use the
useStatefunction to create an additional state variablecurrentPage.src\projects\ProjectsPage.jsx...
function ProjectsPage() {
const [projects, setProjects] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(undefined);
+ const [currentPage, setCurrentPage] = useState(1);
...
}Update the
useEffectmethod to makecurrentPagea dependency and use it when fetching the data.src\projects\ProjectsPage.jsx
...
function ProjectsPage() {
const [projects, setProjects] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(undefined);
const [currentPage, setCurrentPage] = useState(1);
useEffect(() => {
async function loadProjects() {
setLoading(true);
try {
- const data = await projectAPI.get(1);
+ const data = await projectAPI.get(currentPage);
- setProjects(data);
+ if (currentPage === 1) {
+ setProjects(data);
+ } else {
+ setProjects((projects) => [...projects, ...data]);
+ }
} catch (e) {
setError(e.message);
} finally {
setLoading(false);
}
}
loadProjects();
- }, []);
+ }, [currentPage]);
...
}
Implement a
handleMoreClickevent handler and implement it by incrementing the page and then callingloadProjects.src\projects\ProjectsPage.jsx...
function ProjectsPage() {
...
const [currentPage, setCurrentPage] = useState(1);
...
+ const handleMoreClick = () => {
+ setCurrentPage((currentPage) => currentPage + 1);
+ };
...
}Add a
More...button below the<ProjectList />. Display theMore...button only when notloadingand there is not anerrorand handle theMore...button'sclickevent and callhandleMoreClick.src\projects\ProjectsPage.jsx...
function ProjectsPage() {
...
return (
<>
<h1>Projects</h1>
{error && (
<div className="row">
<div className="card large error">
<section>
<p>
<span className="icon-alert inverse "></span>
{error}
</p>
</section>
</div>
</div>
)}
<ProjectList onSave={saveProject} projects={projects} />
+ {!loading && !error && (
+ <div className="row">
+ <div className="col-sm-12">
+ <div className="button-group fluid">
+ <button className="button default" onClick={handleMoreClick}>
+ More...
+ </button>
+ </div>
+ </div>
+ </div>
+ )}
{loading && (
<div className="center-page">
<span className="spinner primary"></span>
<p>Loading...</p>
</div>
)}
</>
);
}
export default ProjectsPage;Verify the application is working by following these steps in your browser.
- Refresh the page.
- A list of projects should appear.
- Click on the
More...button. - Verify that 20 additional projects are appended to the end of the list.
- Click on the
More...button again. - Verify that another 20 projects are appended to the end of the list.
