Lab 26: Custom Hooks
This lab is optional and should only be done if time permits
Objectives
- Move stateful logic out of components by creating Custom Hooks
Steps
This lab is a refactor of the code from the solution of
lab22
so begin by checking out thelab22
solution code and creating a working branch for this lab.git checkout lab22
git checkout -b lab26workingCreate a
projectHooks.ts
file and add the following code.src\projects\projectHooks.ts
import { useState, useEffect } from 'react';
import { projectAPI } from './projectAPI';
import { Project } from './Project';
export function useProjects() {
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
const [currentPage, setCurrentPage] = useState(1);
const [saving, setSaving] = useState(false);
const [savingError, setSavingError] = useState<string | undefined>(
undefined
);
useEffect(() => {
async function loadProjects() {
setLoading(true);
try {
const data = await projectAPI.get(currentPage);
if (currentPage === 1) {
setProjects(data);
} else {
setProjects((projects) => [...projects, ...data]);
}
} catch (e) {
if (e instanceof Error) {
setError(e.message);
}
} finally {
setLoading(false);
}
}
loadProjects();
}, [currentPage]);
const saveProject = (project: Project) => {
setSaving(true);
projectAPI
.put(project)
.then((updatedProject) => {
let updatedProjects = projects.map((p) => {
return p.id === project.id ? new Project(updatedProject) : p;
});
setProjects(updatedProjects);
})
.catch((e) => {
setSavingError(e.message);
})
.finally(() => setSaving(false));
};
return {
projects,
loading,
error,
currentPage,
setCurrentPage,
saving,
savingError,
saveProject,
};
}Notice how this logic was directly lifted out of the
ProjectsPage
component.Refactor the
ProjectsPage
component to remove the logic which is now in the hook and call the hook instead.
Be sure to open the
ProjectsPage.tsx
and not the singularProjectPage.tsx
src\projects\ProjectsPage.ts
-import React, { useState, useEffect } from 'react';
+import React from 'react';
import ProjectList from './ProjectList';
-import { projectAPI } from './projectAPI';
-import { Project } from './Project';
+import { useProjects } from './projectHooks';
function ProjectsPage() {
- const [projects, setProjects] = useState<Project[]>([]);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState<string | undefined>(undefined);
- const [currentPage, setCurrentPage] = useState(1);
- const [saving, setSaving] = useState(false);
- const [savingError, setSavingError] = useState<string | undefined>(undefined);
- useEffect(() => {
- async function loadProjects() {
- setLoading(true);
- try {
- const data = await projectAPI.get(currentPage);
- if (currentPage === 1) {
- setProjects(data);
- } else {
- setProjects((projects) => [...projects, ...data]);
- }
- } catch (e) {
- if (e instanceof Error) {
- setError(e.message);
- }
- } finally {
- setLoading(false);
- }
- }
- loadProjects();
- }, [currentPage]);
+ const {
+ projects,
+ loading,
+ error,
+ setCurrentPage,
+ saveProject,
+ saving,
+ savingError,
+ } = useProjects();
const handleMoreClick = () => {
setCurrentPage((currentPage) => currentPage + 1);
};
- const saveProject = (project: Project) => {
- projectAPI
- .put(project)
- .then((updatedProject) => {
- let updatedProjects = projects.map((p) => {
- return p.id === project.id ? new Project(updatedProject) : p;
- });
- setProjects(updatedProjects);
- })
- .catch((e) => {
- if (e instanceof Error) {
- setError(e.message);
- }
- });
- };
return (
<>
<h1>Projects</h1>
+ {saving && <span className="toast">Saving...</span>}
- {error && (
+ {(error || savingError) && (
<div className="row">
<div className="card large error">
<section>
<p>
<span className="icon-alert inverse "></span>
- {error}
+ {error} {savingError}
</p>
</section>
</div>
</div>
</>
);
}
export default ProjectsPage;
Test the application to verify the loading, saving and error messages are displaying.
Add this line to test the loading spinner
src\projects\projectAPI.ts
get(page = 1, limit = 20) {
return fetch(`${url}?_page=${page}&_limit=${limit}&_sort=name`)
+ .then(delay(2000))
.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.'
);
});
},- Shut down your backend API to test the display of an error message