import React, { createContext, useCallback, useMemo, useRef, useState } from 'react';
import { ToastViewport } from './ToastViewport';
export const ToastContext = createContext(undefined);
let counter = 0;
function nextId() {
counter += 1;
return `toast-${counter}-${Date.now()}`;
}
export function ToastProvider({ children, defaultDuration = 4000 }) {
const [toasts, setToasts] = useState([]);
const timers = useRef(new Map());
const removeToast = useCallback((id) => {
const timer = timers.current.get(id);
if (timer) {
clearTimeout(timer);
timers.current.delete(id);
}
setToasts((prev) => prev.filter((t) => t.id !== id));
}, []);
const addToast = useCallback(
({ message, variant = 'info', duration = defaultDuration }) => {
const id = nextId();
setToasts((prev) => [...prev, { id, message, variant }]);
if (duration > 0) {
const timer = setTimeout(() => removeToast(id), duration);
timers.current.set(id, timer);
}
return id;
},
[defaultDuration, removeToast]
);
const value = useMemo(
() => ({
addToast,
removeToast,
success: (message, opts) => addToast({ ...opts, message, variant: 'success' }),
error: (message, opts) => addToast({ ...opts, message, variant: 'error' }),
info: (message, opts) => addToast({ ...opts, message, variant: 'info' }),
}),
[addToast, removeToast]
);
return (
<ToastContext.Provider value={value}>
{children}
<ToastViewport toasts={toasts} onDismiss={removeToast} />
</ToastContext.Provider>
);
}
import { useContext } from 'react';
import { ToastContext } from './ToastProvider';
export function useToast() {
const ctx = useContext(ToastContext);
if (ctx === undefined) {
throw new Error('useToast must be used within a <ToastProvider>');
}
return ctx;
}
import React, { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
export function ToastViewport({ toasts, onDismiss }) {
const [host] = useState(() => {
const el = document.createElement('div');
el.className = 'toast-host';
return el;
});
useEffect(() => {
document.body.appendChild(host);
return () => {
document.body.removeChild(host);
};
}, [host]);
const content = (
<div className="toast-region" role="region" aria-live="polite" aria-label="Notifications">
{toasts.map((t) => (
<button
key={t.id}
type="button"
className={`toast toast--${t.variant}`}
onClick={() => onDismiss(t.id)}
>
<span className="toast__message">{t.message}</span>
<span className="toast__close" aria-hidden="true">×</span>
</button>
))}
</div>
);
return createPortal(content, host);
}
.toast-region {
position: fixed;
top: 1rem;
right: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
z-index: 9999;
pointer-events: none;
}
.toast {
pointer-events: auto;
display: flex;
align-items: center;
gap: 0.75rem;
min-width: 240px;
padding: 0.75rem 1rem;
border: none;
border-radius: 8px;
color: #fff;
font-size: 0.9rem;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
animation: toast-in 160ms ease-out;
}
.toast--success { background: #16a34a; }
.toast--error { background: #dc2626; }
.toast--info { background: #2563eb; }
.toast__close { margin-left: auto; font-weight: 700; }
@keyframes toast-in {
from { opacity: 0; transform: translateX(12px); }
to { opacity: 1; transform: translateX(0); }
}
This snippet shows a complete, dependency-free toast notification system built on React's own primitives: a context provider that owns the toast queue, a custom hook that exposes an ergonomic API, and a portal renderer that paints toasts above the rest of the app. The split mirrors real responsibilities — state lives in one place, consumers get a tiny surface, and rendering escapes the normal DOM tree.
In ToastProvider, the toast list is held in useState and mutated through useCallback-memoized functions so the context value stays stable across renders. The core primitive is addToast, which generates a unique id, appends a toast, and — unless duration is 0 — schedules an auto-dismiss via setTimeout. Timers are tracked in a useRef map so removeToast can clearTimeout when a user dismisses early, preventing a stale callback from firing against an already-removed id. Convenience wrappers (success, error, info) are derived from addToast and bundled into a useMemo-stabilized value, which matters because an unstable context object would re-render every consumer on each state change. The provider also mounts the ToastViewport so applications only wrap their tree once.
useToast is a thin consumer hook that reads the context with useContext and throws when it is undefined. That guard turns a silent null-reference bug into a clear, actionable error whenever the hook is called outside the provider — a small but important developer-experience safeguard for any context-based library.
ToastViewport is where the portal pattern earns its place. Rendering through createPortal into a dedicated document.body node sidesteps overflow: hidden, transform, and z-index stacking contexts from ancestor elements that would otherwise clip or bury the toasts. The container carries role="region" and aria-live="polite" so screen readers announce new messages without stealing focus, and each toast is a button so it is keyboard-dismissible. The host node is created lazily in a useState initializer and appended in an effect, with cleanup on unmount.
The trade-offs are worth noting: this design keeps everything in memory, so toasts do not survive navigation in non-SPA setups, and the timer-in-ref approach must clear on unmount to avoid leaks. For most apps this pattern is the sweet spot — no external state library, full styling control, and an API as simple as toast.success('Saved').
Related snips
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Semantic HTML Example</title>
Semantic HTML5 elements and accessibility best practices
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["form"]
static values = { delay: { type: Number, default: 250 } }
Debounced live search with Stimulus + Turbo Streams
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
timeout: 15000,
Axios API client with interceptors
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FilterState {
search: string
category: string | null
Zustand for lightweight state management
import SwiftUI
struct ContentView: View {
@State private var username = ""
@State private var isLoggedIn = false
@StateObject private var viewModel = LoginViewModel()
SwiftUI declarative UI with state management
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form Validation Example</title>
<style>
HTML forms with validation and accessibility
Share this code
Here's the card — post it anywhere.