The tea shop's menu has dozens of Add buttons, all reading a cart context. The cart works, but every time anything changes, every button renders again, even though a button only ever calls add and never shows the cart. The starter is that working but wasteful code. Change it so the work matches what each component uses.
Keep the exports and what they do. CartProvider takes children and holds the cart, a list of { id, name, price, quantity } lines, starting empty. useCart() returns the lines. useCartActions() returns { add, remove, clear }: add(tea) adds one of a tea ({ id, name, price }), remove(id) takes out its line, and clear() empties the cart.
The three components stay as they are, and each calls its optional onRender prop every time it renders, which is how the tests count renders:
<CartBadge onRender={...} /> // <p>Cart: 3</p>, from useCart()
<AddButton tea={tea} onRender={...} /> // "Add Sencha", calls add(tea) from useCartActions()
<ClearButton onRender={...} /> // "Empty cart", calls clear() from useCartActions()
What to fix: a component that only calls useCartActions() never renders again when the cart changes. A component that calls useCart() renders once per change, no more. When the component above CartProvider renders again but the cart hasn't changed, no consumer renders again. And useCartActions() returns the same object on every render.
The tests render without Strict Mode, so each update means exactly one render. Don't wrap the components in memo, and don't change them: the fix belongs in the provider and the hooks. The default export, App, is a small menu for the Preview that logs each render to the console.
const sencha = { id: "sencha", name: "Sencha", price: 4 };
const assam = { id: "assam", name: "Assam", price: 3 };
render(
<CartProvider>
<CartBadge />
<AddButton tea={sencha} />
<AddButton tea={assam} />
</CartProvider>,
);
expect(screen.getByText("Cart: 0")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Add Sencha" }));
await user.click(screen.getByRole("button", { name: "Add Assam" }));
await user.click(screen.getByRole("button", { name: "Add Sencha" }));
expect(screen.getByText("Cart: 3")).toBeInTheDocument();
const sencha = { id: "sencha", name: "Sencha", price: 4 };
render(
<CartProvider>
<CartBadge />
<AddButton tea={sencha} />
<ClearButton />
</CartProvider>,
);
await user.click(screen.getByRole("button", { name: "Add Sencha" }));
await user.click(screen.getByRole("button", { name: "Add Sencha" }));
await user.click(screen.getByRole("button", { name: "Empty cart" }));
expect(screen.getByText("Cart: 0")).toBeInTheDocument();
const sencha = { id: "sencha", name: "Sencha", price: 4 };
const assam = { id: "assam", name: "Assam", price: 3 };
let senchaRenders = 0;
let assamRenders = 0;
render(
<CartProvider>
<CartBadge />
<AddButton tea={sencha} onRender={() => senchaRenders++} />
<AddButton tea={assam} onRender={() => assamRenders++} />
</CartProvider>,
);
expect(senchaRenders).toBe(1);
await user.click(screen.getByRole("button", { name: "Add Sencha" }));
await user.click(screen.getByRole("button", { name: "Add Sencha" }));
await user.click(screen.getByRole("button", { name: "Add Assam" }));
expect(screen.getByText("Cart: 3")).toBeInTheDocument();
expect(senchaRenders).toBe(1);
expect(assamRenders).toBe(1);
const sencha = { id: "sencha", name: "Sencha", price: 4 };
let clearRenders = 0;
render(
<CartProvider>
<CartBadge />
<AddButton tea={sencha} />
<ClearButton onRender={() => clearRenders++} />
</CartProvider>,
);
await user.click(screen.getByRole("button", { name: "Add Sencha" }));
await user.click(screen.getByRole("button", { name: "Empty cart" }));
await user.click(screen.getByRole("button", { name: "Add Sencha" }));
expect(screen.getByText("Cart: 1")).toBeInTheDocument();
expect(clearRenders).toBe(1);
const sencha = { id: "sencha", name: "Sencha", price: 4 };
let badgeRenders = 0;
render(
<CartProvider>
<CartBadge onRender={() => badgeRenders++} />
<AddButton tea={sencha} />
<ClearButton />
</CartProvider>,
);
expect(badgeRenders).toBe(1);
await user.click(screen.getByRole("button", { name: "Add Sencha" }));
expect(screen.getByText("Cart: 1")).toBeInTheDocument();
expect(badgeRenders).toBe(2);
await user.click(screen.getByRole("button", { name: "Add Sencha" }));
await user.click(screen.getByRole("button", { name: "Empty cart" }));
expect(screen.getByText("Cart: 0")).toBeInTheDocument();
expect(badgeRenders).toBe(4);
const sencha = { id: "sencha", name: "Sencha", price: 4 };
let badgeRenders = 0;
let addRenders = 0;
function Shell({ children }) {
const [refreshes, setRefreshes] = React.useState(0);
return (
<div>
<button onClick={() => setRefreshes(refreshes + 1)}>Refresh ({refreshes})</button>
<CartProvider>{children}</CartProvider>
</div>
);
}
render(
<Shell>
<CartBadge onRender={() => badgeRenders++} />
<AddButton tea={sencha} onRender={() => addRenders++} />
</Shell>,
);
await user.click(screen.getByRole("button", { name: "Add Sencha" }));
expect(badgeRenders).toBe(2);
await user.click(screen.getByRole("button", { name: "Refresh (0)" }));
await user.click(screen.getByRole("button", { name: "Refresh (1)" }));
expect(screen.getByRole("button", { name: "Refresh (2)" })).toBeInTheDocument();
expect(screen.getByText("Cart: 1")).toBeInTheDocument();
expect(badgeRenders).toBe(2);
expect(addRenders).toBe(1);
const sencha = { id: "sencha", name: "Sencha", price: 4 };
const seen = [];
function Probe() {
const lines = useCart();
const actions = useCartActions();
seen.push(actions);
return <p>{lines.length} lines</p>;
}
render(
<CartProvider>
<Probe />
<AddButton tea={sencha} />
<ClearButton />
</CartProvider>,
);
await user.click(screen.getByRole("button", { name: "Add Sencha" }));
await user.click(screen.getByRole("button", { name: "Empty cart" }));
expect(seen.length).toBe(3);
expect(seen[1]).toBe(seen[0]);
expect(seen[2]).toBe(seen[0]);
function Lines() {
const lines = useCart();
const { add, remove } = useCartActions();
return (
<div>
<p>{lines.map((line) => line.name + ":" + line.quantity).join(",") || "empty"}</p>
<button onClick={() => add({ id: "chai", name: "Chai", price: 2.5 })}>Add chai</button>
<button onClick={() => add({ id: "mint", name: "Mint", price: 1 })}>Add mint</button>
<button onClick={() => remove("chai")}>Remove chai</button>
</div>
);
}
render(
<CartProvider>
<Lines />
</CartProvider>,
);
await user.click(screen.getByRole("button", { name: "Add chai" }));
await user.click(screen.getByRole("button", { name: "Add mint" }));
await user.click(screen.getByRole("button", { name: "Add chai" }));
expect(screen.getByText("Chai:2,Mint:1")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Remove chai" }));
expect(screen.getByText("Mint:1")).toBeInTheDocument();
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 Split a context to save renders. 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.