Lab 23: 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
Create 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 { useState, useEffect } 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 && (
<div className="card large error">
<section>
<p>
<span className="icon-alert inverse "></span>
{error}
</p>
</section>
</div>
)}
+ {savingError && (
+ <div className="card large error">
+ <section>
+ <p>
+ <span className="icon-alert inverse "></span>
+ {savingError}
+ </p>
+ </section>
+ </div>
+ )}
</>
);
}
export default ProjectsPage;
Test the application to verify the loading spinner and saving toast message are displaying.
tipAdd these lines to test the loading spinner and saving message
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.'
);
});
},
put(project: Project) {
return fetch(`${url}/${project.id}`, {
method: 'PUT',
body: JSON.stringify(project),
headers: {
'Content-Type': 'application/json',
},
})
+ .then(delay(2000))
.then(checkStatus)
.then(parseJSON)
.catch((error: TypeError) => {
console.log('log client error ' + error);
throw new Error(
'There was an error updating the project. Please try again.'
);
});
},
...Shut down your backend API to test the display of an error message