Build a Toast that renders into document.body and dismisses itself
Toast takes message (a string, or null when there's nothing to show), onDismiss, and duration in milliseconds (3000 when it isn't given). Wherever it's rendered, even inside a carousel with overflow: hidden and a transform, its DOM goes into document.body through a portal.
It always renders a <div role="status">, empty while message is null, because screen readers only announce changes to a live region that's already on the page. While there's a message, the region holds <p>{message}</p> and a Dismiss button that calls onDismiss.
duration ms after a message appears, call onDismiss once. A new message starts the clock again. A parent that re-renders and passes a new onDismiss function, as the inline arrow below does on every render, must not restart it. Leave no timer waiting once the message is gone or the Toast unmounts.
function Shelf() {
const [message, setMessage] = useState(null);
return (
<div className="carousel">
<button onClick={() => setMessage("Added Sencha to your cart")}>Add Sencha</button>
<Toast message={message} onDismiss={() => setMessage(null)} />
</div>
);
}
import { useEffect } from "react";
import { createPortal } from "react-dom";
export default function Toast({ message, onDismiss, duration = 3000 }) {
// Render into document.body, and call onDismiss after duration ms.
if (!message) return null;
return (
<div role="status">
<p>{message}</p>
<button onClick={onDismiss}>Dismiss</button>
</div>
);
}
- renders into document.body, outside its parent
- keeps an empty status region on the page while there's no message
- Dismiss calls onDismiss
- calls onDismiss once, duration ms after the message appears
- waits 3000 ms when there's no duration
- a parent re-render with a new onDismiss doesn't restart the clock
- a new message starts the clock again
- leaves no timer waiting once the message is gone or the Toast unmounts
The answer
Write it yourself first. Then compare it with a reference solution and the checks it passes.
Checking your sign-in…