
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
- Add the toaster service (
toaster-service.ts
) to your services directory. - Initialize the toaster in your main App component.
- Replace all usages of component-based or Redux-based toast logic with
showToast
. - 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.