- Create an API object that loads data from an REST API
- Update a component to use the API object
- Add Pagination
-
Create the file
src\projects\projectAPI.ts
. -
Create a
projectAPI
object and export it from the file. -
Implement a
get
method that requirespage
andlimit
parameters and sets the default topage = 1
andlimit=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`;
const baseUrl = 'http://localhost:4000'; const url = `${baseUrl}/projects`; function translateStatusToErrorMessage(status: number) { 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: any) { 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: Response) { return response.json(); } // eslint-disable-next-line function delay(ms: number) { return function (x: any): Promise<any> { 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) .catch((error: TypeError) => { console.log('log client error ' + error); throw new Error( 'There was an error retrieving the projects. Please try again.' ); }); }, }; export { projectAPI };
-
Open the file
src\projects\ProjectsPage.tsx
. -
Use the
useState
function to create two additonal state variablesloading
anderror
.... function ProjectsPage() { const [projects, setProjects] = useState<Project[]>(MOCK_PROJECTS); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(undefined); ... }
DO NOT DELETE the file
src\projects\MockProjects.ts
. We will use it in our unit testing. -
Change the
projects
state to be an empty array[]
(be sure to remove the mock data).- import { MOCK_PROJECTS } from './MockProjects'; ... function ProjectsPage() { - const [projects, setProjects] = useState<Project[]>(MOCK_PROJECTS); + const [projects, setProjects] = useState<Project[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(undefined); ... }
-
Implement the loading of the data from the API after the intial component render in a
useEffect
hook. Follow these specifications.- Set state of
loading
totrue
- Call the API:
projectAPI.get(1)
. - If successful, set the returned
data
into the componentsprojects
state variable and set theloading
state variable tofalse
. - If an error occurs, set the returned error's message
error.message
to the componentserror
state andloading
tofalse
.
- Set state of
import React, { Fragment, 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) => {
// 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);
+ 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
HTML
snippet below to format the loading indicator.<div class="center-page"> <span class="spinner primary"></span> <p>Loading...</p> </div>
function ProjectsPage() { const [projects, setProjects] = useState<Project[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(undefined); ... return ( <Fragment> <h1>Projects</h1> <ProjectList onSave={saveProject} projects={projects} /> + {loading && ( + <div className="center-page"> + <span className="spinner primary"></span> + <p>Loading...</p> + </div> + )} </Fragment> ); } export default ProjectsPage;
-
Add these
CSS
styles to center the loading indicator on the page.... //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 theHTML
snippet below. Only display the indicator whenerror
is defined.If you want to try it yourself first before looking at the solution code use the
HTML
snippet 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>
function ProjectsPage() { const [projects, setProjects] = useState<Project[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(undefined); ... return ( <Fragment> <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> )} </Fragment> ); } export default ProjectsPage;
-
Verify the application is working by following these steps in your
Chrome
browser.-
Open the application on
http://localhost:3000
. -
Open
Chrome DevTools
. -
Refresh the page.
-
Then, a list of projects should appear.
-
Click on the
Chrome DevTools
Network
tab. -
Verify the request to
/projects?_page=1&_limit=20&_sort=name
is happening. -
We are using a
delay
function inprojectAPI.get()
to delay the returning of data so it is easier to see the loading indicator. You can remove thedelay
at this point.return 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.
const 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.
... const baseUrl = 'http://localhost:4000'; + const url = `${baseUrl}/projects`; - const url = `${baseUrl}/fail`; ...
-
-
Use the
useState
function to create an additonal state variablecurrentPage
.... function ProjectsPage() { const [projects, setProjects] = useState<Project[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(undefined); + const [currentPage, setCurrentPage] = useState(1); ... }
-
Update the
useEffect
method to makecurrentPage
a dependency and use it when fetching the data.
...
function ProjectsPage() {
const [projects, setProjects] = useState<Project[]>([]);
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
handleMoreClick
event handler and implement it by incrementing the page and then callingloadProjects
.... 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 notloading
and there is not anerror
and handle theMore...
button'sclick
event and callhandleMoreClick
.... function ProjectsPage() { ... return ( <Fragment> <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> )} </Fragment> ); } 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.