The tea shop's site has a light and a dark theme. The button that switches it sits in the header, and the cards that change colour sit deep inside the page. Passing the theme down through every layer as a prop gets old fast, so put it in context.
Export ThemeProvider by name. It takes children and an optional initialTheme, which is "light" or "dark" and is "light" when it isn't given. It keeps the current theme and makes it available to every component inside it, however deep.
Export useTheme() by name. Inside a ThemeProvider it returns { theme, toggleTheme }: theme is "light" or "dark", and toggleTheme() switches to the other one. Every component that calls useTheme() under the same provider sees the same theme, and they all update together. Two separate providers each keep their own theme.
Called outside any ThemeProvider, useTheme() throws an Error whose message is exactly useTheme must be used inside a ThemeProvider, so a missing provider is easy to spot instead of failing later with something like Cannot read properties of null.
function ThemeLabel() {
const { theme } = useTheme();
return <p>Theme: {theme}</p>;
}
function ThemeButton() {
const { toggleTheme } = useTheme();
return <button onClick={toggleTheme}>Switch theme</button>;
}
<ThemeProvider initialTheme="dark">
<header><ThemeButton /></header>
<main><section><ThemeLabel /></section></main>
</ThemeProvider>
The default export, App, is a small page for the Preview that uses both. The tests use their own components like the ones above.
function ThemeLabel() {
const { theme } = useTheme();
return <p>Theme: {theme}</p>;
}
render(
<ThemeProvider>
<ThemeLabel />
</ThemeProvider>,
);
expect(screen.getByText("Theme: light")).toBeInTheDocument();
function ThemeLabel() {
const { theme } = useTheme();
return <p>Theme: {theme}</p>;
}
render(
<ThemeProvider initialTheme="dark">
<ThemeLabel />
</ThemeProvider>,
);
expect(screen.getByText("Theme: dark")).toBeInTheDocument();
function ThemeButton() {
const { theme, toggleTheme } = useTheme();
return <button onClick={toggleTheme}>Theme: {theme}</button>;
}
render(
<ThemeProvider>
<ThemeButton />
</ThemeProvider>,
);
const button = screen.getByRole("button");
await user.click(button);
expect(button).toHaveTextContent("Theme: dark");
await user.click(button);
expect(button).toHaveTextContent("Theme: light");
function ThemeButton() {
const { toggleTheme } = useTheme();
return <button onClick={toggleTheme}>Switch theme</button>;
}
function ThemeLabel() {
const { theme } = useTheme();
return <p>Theme: {theme}</p>;
}
render(
<ThemeProvider>
<header>
<nav>
<ThemeButton />
</nav>
</header>
<main>
<section>
<article>
<ThemeLabel />
</article>
</section>
</main>
</ThemeProvider>,
);
await user.click(screen.getByRole("button", { name: "Switch theme" }));
expect(screen.getByText("Theme: dark")).toBeInTheDocument();
function ThemeButton() {
const { toggleTheme } = useTheme();
return <button onClick={toggleTheme}>Switch theme</button>;
}
function ThemeLabel({ where }) {
const { theme } = useTheme();
return <p>{where}: {theme}</p>;
}
render(
<ThemeProvider>
<ThemeLabel where="Header" />
<ThemeButton />
<div>
<ThemeLabel where="Card" />
</div>
<ThemeLabel where="Footer" />
</ThemeProvider>,
);
await user.click(screen.getByRole("button", { name: "Switch theme" }));
expect(screen.getByText("Header: dark")).toBeInTheDocument();
expect(screen.getByText("Card: dark")).toBeInTheDocument();
expect(screen.getByText("Footer: dark")).toBeInTheDocument();
function ThemeButton({ label }) {
const { theme, toggleTheme } = useTheme();
return <button onClick={toggleTheme}>{label}: {theme}</button>;
}
render(
<div>
<ThemeProvider>
<ThemeButton label="Shop" />
</ThemeProvider>
<ThemeProvider>
<ThemeButton label="Blog" />
</ThemeProvider>
</div>,
);
await user.click(screen.getByRole("button", { name: "Shop: light" }));
expect(screen.getByRole("button", { name: "Shop: dark" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Blog: light" })).toBeInTheDocument();
function Controls() {
const { toggleTheme } = useTheme();
const [showCard, setShowCard] = React.useState(false);
return (
<div>
<button onClick={toggleTheme}>Switch theme</button>
<button onClick={() => setShowCard(true)}>Show card</button>
{showCard && <ThemeLabel />}
</div>
);
}
function ThemeLabel() {
const { theme } = useTheme();
return <p>Theme: {theme}</p>;
}
render(
<ThemeProvider>
<Controls />
</ThemeProvider>,
);
await user.click(screen.getByRole("button", { name: "Switch theme" }));
await user.click(screen.getByRole("button", { name: "Show card" }));
expect(screen.getByText("Theme: dark")).toBeInTheDocument();
function Lost() {
const { theme } = useTheme();
return <p>Theme: {theme}</p>;
}
expect(() => render(<Lost />)).toThrow("useTheme must be used inside a ThemeProvider");
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Theme context. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.