typescript 126 lines · 4 tabs

Toast Notification Queue with a useToast Hook and Context Provider in React

Shared by codesnips Jul 2026
4 tabs
import { createContext } from "react";

export type ToastVariant = "info" | "success" | "error";

export interface Toast {
  id: string;
  message: string;
  variant: ToastVariant;
  duration: number;
}

export type ToastAction =
  | { type: "ADD"; toast: Toast }
  | { type: "REMOVE"; id: string };

export function toastReducer(state: Toast[], action: ToastAction): Toast[] {
  switch (action.type) {
    case "ADD":
      return [...state, action.toast];
    case "REMOVE":
      return state.filter((t) => t.id !== action.id);
    default:
      return state;
  }
}

export interface ToastContextValue {
  toasts: Toast[];
  push: (message: string, opts?: { variant?: ToastVariant; duration?: number }) => string;
  dismiss: (id: string) => void;
}

export const ToastContext = createContext<ToastContextValue | null>(null);
4 files · typescript Explain with highlit

This snippet shows a self-contained toast notification system built around React context, a reducer, and a portal. The design goal is a single global queue that any component can push to without prop-drilling, while keeping each toast's auto-dismiss timer isolated and cancellable.

In ToastContext, the state is modeled as an append-only array of Toast objects reduced by toastReducer. Using a reducer instead of raw useState keeps the mutation logic (ADD, REMOVE) centralized and pure, which matters once timers, manual dismissal, and possible future actions like UPDATE are all touching the same list. The ToastContextValue splits the API into toasts (read state) and imperative push/dismiss methods so the provider owns the queue and consumers only trigger transitions.

ToastProvider wires the reducer to the DOM. The push function is wrapped in useCallback and generates an id with crypto.randomUUID() so repeated renders don't produce new function identities and break memoized children. Each toast schedules its own removal with setTimeout, and the timeout handle is tracked in a timers ref keyed by id; dismiss clears that handle before dispatching REMOVE so a manually-closed toast can't fire a duplicate removal later. The cleanup effect clears every outstanding timer on unmount, which prevents the classic 'setState on unmounted component' leak. Rendering goes through createPortal into document.body, keeping toasts above the normal stacking context regardless of where the provider sits in the tree.

useToast is a thin consumer hook that reads the context and throws when used outside the provider — a deliberate fail-fast guard that turns a silent undefined into an obvious developer error. The ToastViewport and ToastItem components in ToastViewport are pure presentational pieces driven entirely by props, so styling and behavior stay decoupled.

The trade-off is that all toasts share one queue and one provider instance; that is exactly what makes the push API ergonomic from anywhere. A pitfall to watch is duplicate toasts from rapid events — callers that need dedupe should pass a stable key, since this queue intentionally allows repeats. This pattern is the right reach whenever transient, app-wide feedback is needed without coupling it to any particular route or component.


Related snips

typescript
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;

Exponential backoff with jitter for retries

typescript reliability retry
by codesnips 2 tabs
typescript
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

react axios api
by Maya Patel 1 tab
typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface FilterState {
  search: string
  category: string | null

Zustand for lightweight state management

react zustand state-management
by Maya Patel 2 tabs
swift
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

swift swiftui ios
by Sofia Martinez 2 tabs
typescript
import React from "react";

type FallbackProps = {
  error: Error;
  reset: () => void;
};

React Error Boundary + error reporting hook

react frontend error-boundary
by codesnips 3 tabs
erb
<h1>Products</h1>

<%= form_with url: products_path, method: :get,
              data: { turbo_frame: "products_list", turbo_action: "advance" } do |f| %>
  <div class="filters">
    <%= f.text_field :q, value: params[:q], placeholder: "Search products" %>

Frame navigation that targets a specific frame via form_with

rails hotwire turbo
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

Toast Notification Queue with a useToast Hook and Context Provider in React — share card
Link copied