Context Api
At first glance, React's Context API behaves just like we might expect.
import { createContext } from "react";
import { initialState } from "./lib/color-reducer";
const ColorContext = createContext(initialState);
If you hover over ColorContext
, you'll see:
const ColorContext: React.Context<ColorState>;
And if you just needed that you'd be fine.
The problem is that a lot of the time we use the Context APi to help with state management. And part of state management is—you know—updating the state.
And to update the state, you tend to use hooks. And you definitely don't have those hooks outside of components.
The is futher exacerbated by the fact that you can only export ColorContext
from the top-level of the module (e.g. not inside a component where you can use hooks).
And so, it's a bit tempting to say that we want to do something like this:
const ColorContext = createContext<{
state: ColorState;
dispatch: Dispatch<ColorActions>;
}>({ state: initialState });
The problem of course is that you know you're going to get yelled at for not having dispatch
just yet.
One option is to make dispatch
optional, but then you're going to have to confirm it's existence every time you use it—and that not ideal.
const ColorContext = createContext<{
state: ColorState;
dispatch?: Dispatch<ColorActions>;
}>({ state: initialState });
We're going to arrive at a slightly better solution in a bit, but we need to learn a few more things. But, one option is that since we know that we're immediately going to write up dispatch
(or, useState
), we could tell TypeScript, "Hey, trust me, I know what I'm doing."
Ideas that I am not willing to entertain
- Using
any
: This will opt us out of the whole reason we're using TypeScript in the first place.
An escape hatch
This is an escape hatch that should fill you with an icky feeling, but it's mostly okay in this particular case. We can assert that an object meets a given type—even if TypeScript doesn't think that it does.
We did this already in src/index.tsx
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
See that as HTMLElement
. Yea, like there is a chance that it's not on the page. document.getElementById()
handles that telling you that it might return undefined
.
But, we know it's on the page, so we told TypeScript that it's cool. We can do the same thing here too.
const ColorContext = createContext({ state: initialState } as {
state: ColorState;
dispatch: Dispatch<ColorActions>;
});
Now, we can go the rest of the way with the normal ceremony that we're used to.
import { createContext, Dispatch, PropsWithChildren, useReducer } from "react";
import colorReducer, { initialState } from "./lib/color-reducer";
export const ColorContext = createContext({ state: initialState } as {
state: ColorState;
dispatch: Dispatch<ColorActions>;
});
export const ColorProvider = ({ children }: PropsWithChildren) => {
const [state, dispatch] = useReducer(colorReducer, initialState);
return (
<ColorContext.Provider value={{ state, dispatch }}>
{children}
</ColorContext.Provider>
);
};
Don't forget to wrap your application in
The important thing to note is that despite our sneaky move, our type system holds up. We don't have to verify that dispatch
exists constantly and when we go to use state
and dispatch
, everything will have the correct types.
Using the context in your components
And again, we get a lot of the types for free. Let's take it for a spin in src/components/shared/color-change-swatch.tsx
.
import clsx from "clsx";
import { MouseEventHandler, useContext } from "react";
import { ColorContext } from "../../context";
import Button from "./button";
type ColorChangeSwatchProps = {
hexColor: string;
className?: string;
onClick?: MouseEventHandler<HTMLButtonElement>;
};
const ColorChangeSwatch = ({
hexColor,
className,
onClick,
}: ColorChangeSwatchProps) => {
const { dispatch } = useContext(ColorContext);
return (
<Button
className={clsx(
"border-2 border-slate-900 transition-shadow duration-200 ease-in hover:shadow-xl",
className
)}
style={{ backgroundColor: hexColor }}
onClick={() =>
dispatch({ type: "update-hex-color", payload: { hexColor } })
}
>
{hexColor}
</Button>
);
};
export default ColorChangeSwatch;
We can and should unwind the passing through of the onClick
prop, but it's not bothering anyone for now and I'll probably just do that behind the seens as not to waste your precious time.
Wiring up our new state
Now, it both works and it doesn't work at the same time. It totally works, but src/components/application.tsx
is using a different reducer right now.
This is an easy fix:
const Application = () => {
const { state, dispatch } = useContext(ColorContext);
const hexColor = state.hexColor;
return (
<div className="grid max-w-3xl grid-cols-1 gap-8 p-8 pb-40 mx-auto dark:bg-slate-900 dark:text-white sm:grid-cols-2">
<ColorPicker
hexColor={hexColor}
onChange={(e) =>
dispatch({
type: "update-hex-color",
payload: { hexColor: e.target.value },
})
}
/>
<AdjustColors hexColor={hexColor} dispatch={dispatch} />
<RelatedColors hexColor={hexColor} />
<SavedColors hexColor={hexColor} dispatch={dispatch} />
</div>
);
};