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.
Context API for villains
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:
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;Identify the state to be passed down to child components. Import the context there.
// src/villains/Villains.tsx import { VillainsContext } from "./VillainsContext"; // ... const { villains, status, getError } = useGetEntity();Wrap the UI with the context’s Provider component, assign the state to be passed down to the
valueprop:// src/villains/Villains.tsx (the sharer) <VillainsContext.Provider value={villains}> <Routes>...</Routes> </VillainsContext.Provider>In any component that is needing the state, consume Context API; import the
useContexthook, and the context object:// src/villains/VillainList.tsx import { useContext } from "react"; import { VillainsContext } from "./VillainsContext";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:
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).
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.
Update the component test to also use the context provider when mounting.
Update the RTL test to also use context provider when rendering.
Using a custom hook for sharing context
The context, created at VillainsContext is acting as the arbiter. Villains.tsx uses the context to share villains state to its child VillainList. VillainList acquires the state from VillainsContext. Instead of VillainsContext we can use a hook that will reduce the imports.
Remove src/villains/VillainsContext.ts and instead create a hook src/hooks/useVillainsContext.ts. The first half of the hook is the same as VillainsContext. We add a setter, so that the components that get passed down the state also gain the ability to set it. Additionally we manage the state and effects related to the hook’s functionality within the hook and return only the value(s) that components need.
At Villains.tsx the only change is the import location of VillainsContext.
Similarly, at VillainsList component test, only the import changes. We are using the hook useVillainsContext. The same applies to VillainList.test.tsx.
At VillainList.tsx, as in Villains.tsx the import changes. Additionally, we do not need to import useContext. We now de-structure villains; const [villains] = useVillainsContext(). If we needed to we could also get the setter out of the hook to set the context.
Summary & Takeaways
Context api lets us pass a value deep into the component tree, without explicitly threading it through every component.
Use a custom hook, manage state and effects within the hook, and only return the values that the components need which may be a value and a setValue.
With
cy.interceptand MSW we checked that a network request goes out versus checking that the operation caused a hook to be called. Consequently changing the hooks had no impact on the tests, or the functionality. This is why we want to test at a slightly higher level of abstraction, and why we want to verify the consequences of the implementation vs the implementation details themselves.
Last updated