Build a toast notification system
mediumState & Data+200 XP
A button triggers a toast notification. Triggering it again while toasts are already showing stacks another one alongside them, rather than replacing it.
Each toast disappears on its own after 2 seconds, or immediately if its own close button is clicked.
Examples
// Click "Show toast" three times in quick succession:
// [ toast 1 (×) ]
// [ toast 2 (×) ]
// [ toast 3 (×) ]
// 2 seconds later, with no interaction: all three disappear.
// Click × on toast 2 immediately → only toast 2 goes; 1 and 3 still auto-dismiss.Each toast tracks its own 2s timer — closing one doesn't reset the others.
function showToast() {
const id = nextId++;
setToasts((current) => [...current, { id, text: "Saved!" }]);
setTimeout(() => {
setToasts((current) => current.filter((t) => t.id !== id));
}, 2000);
}
function closeToast(id) {
setToasts((current) => current.filter((t) => t.id !== id));
}Filtering by id (not index) is what lets the auto-dismiss and the manual close coexist safely.
What it has to do
- A button `data-testid="show-toast"` adds one toast each time it is clicked.
- Each toast renders as `data-testid="toast"` inside a `data-testid="toast-container"`.
- Multiple toasts triggered in quick succession all stack simultaneously — the count of visible toasts matches the number of clicks.
- Each toast has its own `data-testid="toast-close"` button that removes only that toast.
- A toast that is not manually closed removes itself automatically after 2000ms.
Notes
- Keep a stable id per toast — closing by array index breaks when a mid-stack toast auto-dismisses first and shifts the indices.
- If you store the timer id on the toast you can `clearTimeout(toast.timer)` in `closeToast()` to drop the dead callback — not required by the test, but avoids the setState-after-unmount warning in strict mode.
- Position the container with `position: fixed` top/right (or bottom/center) so toasts float over whatever page content is beneath. Accessibility: wrap it in `role="status" aria-live="polite"` so screen readers announce new toasts.
- A production toast system usually caps the stack at 3-5 and queues the rest — a lesson learned the first time an API error loop produces 400 toasts in 10 seconds.
Stuck? The react course covers the ideas this problem is built on.
import { useState } from "react"; let nextId = 1; export default function App() { const [toasts, setToasts] = useState([]); function showToast() { const id = nextId++; setToasts((current) => [...current, { id, message: "Action completed" }]); // TODO: this toast should remove itself automatically after 2000ms. } function closeToast(id) { // TODO: remove just this toast from state. } return ( <div style={{ fontFamily: "sans-serif", padding: 16 }}> <button data-testid="show-toast" onClick={showToast}> Show toast </button> <div data-testid="toast-container" style={{ marginTop: 16, display: "flex", flexDirection: "column", gap: 8 }} > {toasts.map((toast) => ( <div key={toast.id} data-testid="toast" style={{ padding: 12, background: "#333", color: "white", borderRadius: 6 }} > {toast.message} <button data-testid="toast-close" onClick={() => closeToast(toast.id)} style={{ marginLeft: 12 }} > × </button> </div> ))} </div> </div> ); }
- 1.A button `data-testid="show-toast"` adds one toast each time it is clicked.
- 2.Each toast renders as `data-testid="toast"` inside a `data-testid="toast-container"`.
- 3.Multiple toasts triggered in quick succession all stack simultaneously — the count of visible toasts matches the number of clicks.
- 4.Each toast has its own `data-testid="toast-close"` button that removes only that toast.
- 5.A toast that is not manually closed removes itself automatically after 2000ms.
5 automated assertions run against your rendered component. The exact assertion names are hidden so you build to the behaviour, not to the test.
5 tests · +200 XP
Discussion
Sign in to postSign in to join the conversation.Sign in
No comments yet. Be the first to say something.