export const MAX_VISIBLE = 4;
const DEFAULTS = {
variant: "info",
duration: 4000,
};
export function createToast(payload) {
return {
id: crypto.randomUUID(),
message: "",
...DEFAULTS,
...payload,
};
}
export function toastReducer(state, action) {
switch (action.type) {
case "ADD": {
const next = [action.toast, ...state];
return next.slice(0, MAX_VISIBLE);
}
case "REMOVE":
return state.filter((t) => t.id !== action.id);
case "CLEAR":
return [];
default:
return state;
}
}
import React, {
createContext,
useCallback,
useContext,
useMemo,
useReducer,
} from "react";
import { toastReducer, createToast } from "./toastReducer";
import { ToastViewport } from "./ToastViewport";
const ToastContext = createContext(null);
export function ToastProvider({ children }) {
const [toasts, dispatch] = useReducer(toastReducer, []);
const remove = useCallback((id) => {
dispatch({ type: "REMOVE", id });
}, []);
const show = useCallback((message, options = {}) => {
const toast = createToast({ message, ...options });
dispatch({ type: "ADD", toast });
return toast.id;
}, []);
const clear = useCallback(() => dispatch({ type: "CLEAR" }), []);
const api = useMemo(() => ({ show, remove, clear }), [show, remove, clear]);
return (
<ToastContext.Provider value={api}>
{children}
<ToastViewport toasts={toasts} onDismiss={remove} />
</ToastContext.Provider>
);
}
export function useToast() {
const ctx = useContext(ToastContext);
if (!ctx) {
throw new Error("useToast must be used within a <ToastProvider>");
}
return ctx;
}
import React, { useEffect } from "react";
function ToastItem({ toast, onDismiss }) {
const { id, message, variant, duration } = toast;
useEffect(() => {
if (!duration) return undefined; // duration 0 = sticky
const timer = setTimeout(() => onDismiss(id), duration);
return () => clearTimeout(timer);
}, [id, duration, onDismiss]);
return (
<div className={`toast toast--${variant}`}>
<span className="toast__message">{message}</span>
<button
type="button"
className="toast__close"
aria-label="Dismiss notification"
onClick={() => onDismiss(id)}
>
×
</button>
</div>
);
}
export function ToastViewport({ toasts, onDismiss }) {
if (toasts.length === 0) return null;
return (
<div className="toast-viewport" role="status" aria-live="polite">
{toasts.map((toast) => (
<ToastItem key={toast.id} toast={toast} onDismiss={onDismiss} />
))}
</div>
);
}
A toast system needs to solve two problems at once: any component anywhere in the tree must be able to fire a notification, and the notifications themselves must be managed as a bounded, self-expiring queue. This snippet models that with a single reducer-backed store exposed through context, plus a small headless renderer that reads the queue and dismisses entries on their own timers.
In toastReducer.js, the state is just an ordered array of toast objects. The ADD action prepends a new toast and then applies MAX_VISIBLE by slicing the tail, which caps how many toasts can pile up during a burst of activity — older ones are silently dropped rather than allowed to grow without bound. Each toast carries an id created with crypto.randomUUID() so that REMOVE can target a specific entry regardless of its position, and variant/duration are normalized with defaults at creation time so the renderer never has to guess. Keeping this logic in a pure reducer makes the transitions easy to reason about and trivial to test in isolation.
ToastContext.jsx wires the reducer into the tree with useReducer and splits the exposed API into stable dispatch-derived callbacks. The show function is wrapped in useCallback so its identity stays stable across renders — important because consumers often list it as a useEffect dependency. The provider value is memoized with useMemo to avoid re-rendering every consumer whenever the toast array changes. A convenience useToast hook throws when used outside the provider, which turns a silent no-op into an obvious developer error.
ToastViewport.jsx is the presentational layer. It maps over toasts and, critically, each ToastItem owns its own dismissal timer via useEffect, clearing it on unmount so a manually-dismissed toast never fires a stale remove. A duration of 0 opts a toast out of auto-dismiss for things like errors that require acknowledgement. The container uses role="status" and aria-live="polite" so screen readers announce new messages without stealing focus. Reaching for this pattern makes sense once toasts are triggered from many unrelated places; the reducer centralizes queue rules while per-item timers keep lifecycle logic local and leak-free.
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 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
import { Application } from "@hotwired/stimulus"
import FormSubmitController from "./controllers/form_submit_controller"
const application = Application.start()
application.debug = false
Disable submit button while Turbo form is submitting
Share this code
Here's the card — post it anywhere.