Lab 24: React Query Refactor
Objectives
- Install React Query and React Query Devtools.
- Configure React Query Client provider and React Query Devtools.
- Remove the
saveProject
function andonSave
props. - Refactor the
useProjects
custom hook to use React Query'suseQuery
. - Use React Query’s
isFetching
status to show when data is refreshing in the background. - Create and use a
useSaveProject
custom hook using React Query’suseMutation
.
Steps
Install React Query and React Query Devtools.
Run the follow commands at a command-prompt or terminal (be sure you are in the projectpilot directory).
npm
npm install @tanstack/react-query@5
npm install @tanstack/react-query-devtools@5OR
yarn
yard add @tanstack/react-query@5
yard add @tanstack/react-query-devtools@5
Configure React Query Client provider and React Query Devtools.
- Wrap the
App
component in aQueryClientProvider
and add theReactQueryDevtools
inside of the provider. Also, create aQueryClient
and pass it to theQueryClientProvider
.
Remove the saveProject
function and onSave
props
Remove all the code highlighted from the following files
src/projects/ProjectsPage.jsx
...
<ProjectList projects={projects}
- onSave={saveProject}
/>
...src/projects/ProjectList.jsx
...
ProjectList.propTypes = {
projects: PropTypes.arrayOf(PropTypes.instanceOf(Project)).isRequired,
- onSave: PropTypes.func.isRequired,
};
export default ProjectList;...
function ProjectList({ projects
- , onSave
}) {
...<ProjectForm
project={project}
- onSave={onSave}
onCancel={cancelEditing}
/>src/projects/ProjectForm.jsx
...
function ProjectForm({
project: initialProject,
- onSave,
onCancel,
}) {
...
ProjectForm.propTypes = {
project: PropTypes.instanceOf(Project),
onCancel: PropTypes.func.isRequired,
- onSave: PropTypes.func.isRequired,
};
export default ProjectForm;const handleSubmit = (event) => {
event.preventDefault();
if (!isValid()) return;
- onSave(project);
};We will get the save functionality working again later in the lab.
Refactor the useProjects
custom hook to use React Query's useQuery
.
We will also use React Query’s
isFetching
status to show when data is refreshing in the background.
DELETE ALL the code in
src/projects/projectHooks.js
Add the following code. Notice that is significantly less code.
src/projects/projectHooks.js
import { useState } from 'react';
import { projectAPI } from './projectAPI';
import {
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query';
import { Project } from './Project';
export function useProjects() {
const [page, setPage] = useState(0);
let queryInfo = useQuery({
queryKey: ['projects', page],
queryFn: () => projectAPI.get(page + 1),
keepPreviousData: true,
// staleTime: 5000,
});
console.log(queryInfo);
return { ...queryInfo, page, setPage };
}DELETE ALL the code in
src/projects/ProjectsPage.jsx
Update the
ProjectsPage.jsx
to use the React Query based custom hook.src/projects/ProjectsPage.jsx
import React, { useEffect, useState } from 'react';
import { useProjects } from './projectHooks';
import ProjectList from './ProjectList';
function ProjectsPage() {
const {
data,
isPending,
error,
isError,
isFetching,
page,
setPage,
isPreviousData,
} = useProjects();
return (
<>
<h1>Projects</h1>
{data ? (
<>
{isFetching && !isPending && (
<span className="toast">Refreshing...</span>
)}
<ProjectList projects={data} />
<div className="row">
<div className="col-sm-4">Current page: {page + 1}</div>
<div className="col-sm-4">
<div className="button-group right">
<button
className="button "
onClick={() => setPage((oldPage) => oldPage - 1)}
disabled={page === 0}
>
Previous
</button>
<button
className="button"
onClick={() => {
if (!isPreviousData) {
setPage((oldPage) => oldPage + 1);
}
}}
disabled={data.length != 10}
>
Next
</button>
</div>
</div>
</div>
</>
) : isPending ? (
<div className="center-page">
<span className="spinner primary"></span>
<p>Loading...</p>
</div>
) : isError && error instanceof Error ? (
<div className="row">
<div className="card large error">
<section>
<p>
<span className="icon-alert inverse "></span>
{error.message}
</p>
</section>
</div>
</div>
) : null}
</>
);
}
export default ProjectsPage;
// this commented code is unnecessary it's just here to show you the pattern
// return (
// <>
// <h1>Header</h1>
// {data ? (
// <p>data</p>
// ) : isLoading ? (
// <p>Loading...</p>
// ) : isError ? (
// <p>Error Message</p>
// ) : null}
// </>
// );To make it easier to see the pagination change the page size to 10 records. Delay the query to make it easier to see the loading indicator.
src/projects/projectAPI.js
const projectAPI = {
get(page = 1,
- limit = 20
+ limit = 10
) {
return (
fetch(`${url}?_page=${page}&_limit=${limit}&_sort=name`)
.then(delay(2000))
.then(checkStatus)
.then(parseJSON)
.catch((error) => {
console.log('log client error ' + error);
throw new Error(
'There was an error retrieving the projects. Please try again.'
);
})
);
},Test the projects page and verify that the initial load and pagination work.
Create and use a useSaveProject
custom hook using React Query’s useMutation
.
Add the
useSaveProject
custom hook.src/projects/projectHooks.js
// existing code
...
export function useSaveProject() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (project) => projectAPI.put(project),
onSuccess: () => queryClient.invalidateQueries(['projects']),
});
}Update the
ProjectForm
to use theuseSaveProject
hook.src/projects/ProjectForm.jsx
import React, { SyntheticEvent, useState } from 'react';
import { Project } from './Project';
+ import { useSaveProject } from './projectHooks';
...
function ProjectForm({ project: initialProject,
onCancel }) {
...
+ const { mutate: saveProject, isPending } = useSaveProject();
const handleSubmit = (event) => {
event.preventDefault();
if (!isValid()) return;
+ saveProject(project);
};
...
return (
<form className="input-group vertical" onSubmit={handleSubmit}>
+ {isPending && <span className="toast">Saving...</span>}
<label htmlFor="name">Project Name</label>
...
}
export default ProjectForm;Test the projects page. Edit a project and save it and verify the change works and you see the saving toast indicator near the bottom of the page.