Back to posts
Singleton Based API

Singleton Based API

Matan Shaviro / May 10, 2025

Overview

A common challenge in large React applications is displaying toast notifications from outside React components—such as from utility functions, services, or Redux middleware. Previously, toast notifications were tightly coupled to React components, limiting their flexibility.

The new Global Toaster Service solves this by providing a singleton-based API to trigger toasts from anywhere in the app, while maintaining strong TypeScript types and React best practices.


Motivation

  • Decoupling: Allow toast notifications to be triggered from non-React code (e.g., services, utils).
  • Reusability: Centralize toast logic for easier maintenance and consistency.
  • Type Safety: Ensure all toast invocations are strongly typed.

Implementation Details

1. The Toaster Service

A new file, toaster-service.ts, is introduced. It exposes two main functions:

  • initializeToaster: Called once (from the main App component) to register the actual toast-displaying function.
  • showToast: Can be called from anywhere to display a toast, as long as initialization has occurred.

Example (pseudo-code):

// src/services/toaster-service.ts

import { ToastOptions, ToastVariant } from '../components/ui/toast'

type ToastFunction = (
  message: string,
  variant: ToastVariant,
  options?: Partial<ToastOptions>
) => void

let toaster: ToastFunction | null = null

export function initializeToaster(addToast: ToastFunction) {
  toaster = addToast
}

export function showToast(
  message: string,
  variant: ToastVariant,
  options?: Partial<ToastOptions>
) {
  if (!toaster) {
    throw new Error('Toaster not initialized')
  }
  toaster(message, variant, options)
}
  • Singleton pattern ensures only one instance of the toaster function is used.
  • TypeScript types guarantee type safety.

2. Initializing the Toaster in the App

In your main App component, you must initialize the toaster service with the actual addToast function from your toast provider context.

Example:

import { useToast } from '../components/ui/toast'
import { initializeToaster } from '../services/toaster-service'

const App = () => {
  const { toast } = useToast()

  useEffect(() => {
    initializeToaster((message, variant, options) => {
      toast({
        title: message,
        variant,
        ...options
      })
    })
  }, [toast])

  // ...rest of your App
}
  • This ensures the global service is ready to use after the app mounts.

3. Using the Global Toaster

Now, you can call showToast from anywhere in your codebase—even outside React components.

Example:

import { showToast } from '../services/toaster-service'
import { ToastVariant } from '../components/ui/toast'

function someUtilityFunction() {
  showToast('Operation successful!', ToastVariant.Success)
}

4. Practical Example: API Service with Toast Notifications

A common use case is showing toast notifications for API responses:

// src/services/api-service.ts
import { showToast } from './toaster-service'
import { ToastVariant } from '../components/ui/toast'

export async function fetchUserData(userId: string) {
  try {
    const response = await fetch(`/api/users/${userId}`)

    if (!response.ok) {
      const error = await response.json()
      showToast(`Failed to fetch user: ${error.message}`, ToastVariant.Error)
      return null
    }

    return await response.json()
  } catch (error) {
    showToast('Network error occurred', ToastVariant.Error)
    return null
  }
}

export async function updateUserProfile(userId: string, data: UserProfileData) {
  try {
    const response = await fetch(`/api/users/${userId}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    })

    if (response.ok) {
      showToast('Profile updated successfully', ToastVariant.Success, {
        duration: 3000,
        action: {
          label: 'View',
          onClick: () => (window.location.href = `/profile/${userId}`)
        }
      })
      return true
    } else {
      const error = await response.json()
      showToast(`Update failed: ${error.message}`, ToastVariant.Error)
      return false
    }
  } catch (error) {
    showToast('Network error occurred', ToastVariant.Error)
    return false
  }
}

This approach allows your API service to provide user feedback without direct access to React components or contexts.


5. Another Example: Form Validation Service

You can also use the global toaster in a validation service:

// src/services/validation-service.ts
import { showToast } from './toaster-service'
import { ToastVariant } from '../components/ui/toast'

export function validatePassword(password: string): boolean {
  const requirements = [
    {
      test: password.length >= 8,
      message: 'Password must be at least 8 characters'
    },
    {
      test: /[A-Z]/.test(password),
      message: 'Password must contain an uppercase letter'
    },
    {
      test: /[a-z]/.test(password),
      message: 'Password must contain a lowercase letter'
    },
    { test: /[0-9]/.test(password), message: 'Password must contain a number' },
    {
      test: /[^A-Za-z0-9]/.test(password),
      message: 'Password must contain a special character'
    }
  ]

  const failedRequirements = requirements.filter(req => !req.test)

  if (failedRequirements.length > 0) {
    showToast(failedRequirements[0].message, ToastVariant.Warning)
    return false
  }

  return true
}

export function validateForm(
  formData: Record<string, any>,
  schema: Record<string, (value: any)=> boolean>
): boolean {
  for (const [field, validator] of Object.entries(schema)) {
    if (!validator(formData[field])) {
      showToast(`Invalid value for ${field}`, ToastVariant.Error)
      return false
    }
  }

  return true
}

Migration Steps

  1. Add the toaster service (toaster-service.ts) to your services directory.
  2. Initialize the toaster in your main App component.
  3. Replace all usages of component-based or Redux-based toast logic with showToast.
  4. Remove any now-unused Redux state, actions, and selectors related to toasts.

Best Practices

  • Always initialize the toaster service before calling showToast.
  • Use strong types for all toast parameters.
  • Prefer useCallback/useMemo when passing toast functions as props.
  • Remove legacy toast logic to avoid confusion.

The new Global Toaster Service makes toast notifications more flexible, maintainable, and type-safe across your React application. By decoupling toast logic from React components and state management, you can now trigger notifications from anywhere in your codebase with confidence.