ch19-Villains-context-api
Before starting this section, make sure to go through the prerequisite where we update the application to be more generic between Heroes and Villains.
In this chapter we will mirror heroes into villains, and apply the Context api to villains. Context api lets us pass a value deep into the component tree, without explicitly threading it through every component. This will give us a nice contrast between passing the state as prop to child components, versus using the context to share the state down the component tree.
Heroes.tsx
passes heroes
as a prop to HeroList.tsx
.HeroDetail
gets hero
{id, name, description}
state from the url with useParams
& useSearchParams
.This is the extent of state management in our app. We do not really need context, but we will explore how things can be different with the context api.
At the moment we have a full mirror of heroes to villains, functioning and being tested exactly the same way. We will however modify the villains group and take advantage of Context api while doing so.
Villains.tsx
passes villains
as a prop to VillainList.tsx
. We will instead use the Context api so that villains
is available in all components under Villains.txt
.Here are the general steps with Context api:
- 1.Create the context and export it. Usually this is in a separate file, acting as an arbiter.// src/villains/VillainsContext.tsx (the common node)import { Villain } from "models/Villain";import { createContext } from "react";const VillainsContext = createContext<Villain[]>([]);export default VillainsContext;
- 2.Identify the state to be passed down to child components. Import the context there.// src/villains/Villains.tsximport { VillainsContext } from "./VillainsContext";// ...const { villains, status, getError } = useGetEntity();
- 3.Wrap the UI with the context’s Provider component, assign the state to be passed down to the
value
prop:// src/villains/Villains.tsx (the sharer)<VillainsContext.Provider value={villains}><Routes>...</Routes></VillainsContext.Provider> - 4.In any component that is needing the state, consume Context API; import the
useContext
hook, and the context object:// src/villains/VillainList.tsximport { useContext } from "react";import { VillainsContext } from "./VillainsContext"; - 5.Call useContext with the shared context, assign to a var:// src/villains/VillainList.tsx (the sharee)import { useContext } from "react";import { VillainsContext } from "./VillainsContext";// ..const villains = useContext(VillainsContext);
Following step 1, we create the context at a new file, and export it:
// src/villains/VillainsContext.ts
import { Villain } from "models/Villain";
import { createContext } from "react";
const VillainsContext = createContext<Villain[]>([]);
export default VillainsContext;
In our example, from
Villains.tsx
, we are passing villains
to VillainDetail.tsx
. We get villains
from the hook useGetEntity
. We are currently using a prop to pass villains
to VillainDetail.tsx
, and we want to instead use the context api. So we import the context, and wrap the routes with the context provider, which has a value
prop with villains
assigned to it (Steps 2, 3).// src/villains/Villains.tsx
import { useState } from "react";
import { useNavigate, Routes, Route } from "react-router-dom";
import ListHeader from "components/ListHeader";
import ModalYesNo from "components/ModalYesNo";
import PageSpinner from "components/PageSpinner";
import ErrorComp from "components/ErrorComp";
import VillainList from "./VillainList";
import VillainDetail from "./VillainDetail";
import { useGetEntities } from "hooks/useGetEntities";
import { useDeleteEntity } from "hooks/useDeleteEntity";
import { Villain } from "models/Villain";
import VillainsContext from "./VillainsContext";
export default function Villains() {
const [showModal, setShowModal] = useState<boolean>(false);
const { entities: villains, status, getError } = useGetEntities("villains");
const [villainToDelete, setVillainToDelete] = useState<Villain | null>(null);
const { deleteEntity: deleteVillain, isDeleteError } =
useDeleteEntity("villain");
const navigate = useNavigate();
const addNewVillain = () => navigate("/villains/add-villain");
const handleRefresh = () => navigate("/villains");
const handleCloseModal = () => {
setVillainToDelete(null);
setShowModal(false);
};
const handleDeleteVillain = (villain: Villain) => () => {
setVillainToDelete(villain);
setShowModal(true);
};
const handleDeleteFromModal = () => {
villainToDelete ? deleteVillain(villainToDelete) : null;
setShowModal(false);
};
if (status === "loading") {
return <PageSpinner />;
}
if (getError || isDeleteError) {
return <ErrorComp />;
}
return (
<div data-cy="villains">
<ListHeader
title="Villains"
handleAdd={addNewVillain}
handleRefresh={handleRefresh}
/>
<div>
<div>
<VillainsContext.Provider value={villains}>
<Routes>
<Route
path=""
element={
<VillainList handleDeleteVillain={handleDeleteVillain} />
}
/>
<Route path="/add-villain" element={<VillainDetail />} />
<Route path="/edit-villain/:id" element={<VillainDetail />} />
<Route
path="*"
element={
<VillainList handleDeleteVillain={handleDeleteVillain} />
}
/>
</Routes>
</VillainsContext.Provider>
</div>
</div>
{showModal && (
<ModalYesNo
message="Would you like to delete the villain?"
onNo={handleCloseModal}
onYes={handleDeleteFromModal}
/>
)}
</div>
);
}
VillainsList.tsx
needs to be passed down the state via context versus a prop. So we import the context, and importuseContext
from React. We invoke useContext
with the shared context as its argument, and assign to a variable villains
(Steps 4, 5). Now, instead of the prop, we are getting the state from the VillainsContext
.// src/villains/VillainList.tsx
import { useNavigate } from "react-router-dom";
import CardContent from "components/CardContent";
import ButtonFooter from "components/ButtonFooter";
import { FaEdit, FaRegSave } from "react-icons/fa";
import {
ChangeEvent,
MouseEvent,
useTransition,
useEffect,
useState,
useDeferredValue,
} from "react";
import { useContext } from "react";
import { Villain } from "models/Villain";
import VillainsContext from "./VillainsContext";
type VillainListProps = {
handleDeleteVillain: (
villain: Villain
) => (e: MouseEvent<HTMLButtonElement>) => void;
};
export default function VillainList({ handleDeleteVillain }: VillainListProps) {
const villains = useContext(VillainsContext);
const deferredVillains = useDeferredValue(villains);
const isStale = deferredVillains !== villains;
const [filteredVillains, setFilteredVillains] = useState(deferredVillains);
const navigate = useNavigate();
const [isPending, startTransition] = useTransition();
// needed to refresh the list after deleting a villain
useEffect(() => setFilteredVillains(deferredVillains), [deferredVillains]);
const handleSelectVillain = (villainId: string) => () => {
const villain = deferredVillains.find((h: Villain) => h.id === villainId);
navigate(
`/villains/edit-villain/${villain?.id}?name=${villain?.name}&description=${villain?.description}`
);
};