Skip to main content

Command Palette

Search for a command to run...

Building a Scalable Global Toast System in React Native (With Clean Architecture)

Updated
4 min read
Building a Scalable Global Toast System in React Native (With Clean Architecture)
A

I am developer from India.

When building mobile apps, one pattern shows up everywhere: ephemeral feedback — success messages, errors, warnings, etc.

A well-designed Toast system solves this cleanly.

In this article, we’ll:

  • Build a global toast system

  • Support multiple concurrent toasts

  • Add auto-dismiss + manual close

  • Keep the implementation clean, scalable, and interview-ready


🚨 The Problem With Naive Implementations

Most developers start with:

  • useState boolean (isVisible)

  • One toast at a time

  • Manual timers in components

This breaks down quickly:

  • ❌ No support for multiple toasts

  • ❌ Logic duplicated across screens

  • ❌ Hard to control globally


✅ What We Want (Design Goals)

A proper system should:

  • Be globally accessible

  • Support stacking (multiple toasts)

  • Handle auto-dismiss

  • Allow manual close

  • Be decoupled from UI logic


🧠 Architecture Overview

We’ll use:

  • Context API → global access

  • useState → reactive UI updates

  • Unique id → track each toast

  • setTimeout → lifecycle management


🏗️ Step 1: Create Toast Context

import { createContext, useContext } from "react";

const ToastContext = createContext(null);

export const useToast = () => {
  const context = useContext(ToastContext);
  if (!context) {
    throw new Error("useToast must be used within ToastProvider");
  }
  return context;
};

🏗️ Step 2: Build Toast Provider

This is the core engine.

import React, { useState } from "react";
import { View, Text, Pressable } from "react-native";

let idCounter = 0;

export const ToastProvider = ({ children }) => {
  const [toasts, setToasts] = useState([]);

  const showToast = ({
    content,
    duration = 3000,
    backgroundColor = "#333",
  }) => {
    const id = idCounter++;

    const newToast = { id, content, duration, backgroundColor };

    // Add toast
    setToasts((prev) => [...prev, newToast]);

    // Auto remove
    setTimeout(() => {
      removeToast(id);
    }, duration);
  };

  const removeToast = (id) => {
    setToasts((prev) => prev.filter((t) => t.id !== id));
  };

  return (
    <ToastContext.Provider value={{ showToast }}>
      {children}

      {/* Toast Container */}
      <View
        style={{
          position: "absolute",
          bottom: 50,
          left: 20,
          right: 20,
        }}
      >
        {toasts.map((toast) => (
          <View
            key={toast.id}
            style={{
              backgroundColor: toast.backgroundColor,
              padding: 12,
              borderRadius: 8,
              marginBottom: 10,
              flexDirection: "row",
              justifyContent: "space-between",
              alignItems: "center",
            }}
          >
            <Text style={{ color: "white" }}>{toast.content}</Text>

            <Pressable onPress={() => removeToast(toast.id)}>
              <Text style={{ color: "white" }}>✕</Text>
            </Pressable>
          </View>
        ))}
      </View>
    </ToastContext.Provider>
  );
};

🏗️ Step 3: Use It Anywhere

const HomeScreen = () => {
  const { showToast } = useToast();

  return (
    <Pressable
      onPress={() =>
        showToast({
          content: "Toast triggered 🚀",
          backgroundColor: "green",
          duration: 2000,
        })
      }
    >
      <Text>Show Toast</Text>
    </Pressable>
  );
};

🏗️ Step 4: Wrap Your App

export default function App() {
  return (
    <ToastProvider>
      <HomeScreen />
    </ToastProvider>
  );
}

⚙️ Why This Implementation Is Better

1. State-driven UI

We use:

const [toasts, setToasts] = useState([]);

This ensures UI re-renders automatically.


2. Immutable Updates

setToasts(prev => [...prev, newToast]);

No mutation → predictable behavior.


3. Independent Lifecycle

Each toast:

  • Has its own id

  • Has its own timer

This enables:

  • Multiple toasts

  • Independent removal


4. Global Access Pattern

const { showToast } = useToast();

No prop drilling. Clean API.


🚀 Optimizations (Senior-Level Improvements)

🔹 1. Prevent Memory Leaks

Store timers and clear them on unmount:

const timers = useRef({});

timers.current[id] = setTimeout(() => {
  removeToast(id);
}, duration);

// cleanup
useEffect(() => {
  return () => {
    Object.values(timers.current).forEach(clearTimeout);
  };
}, []);

🔹 2. Limit Max Toasts

const MAX_TOASTS = 3;

setToasts(prev => {
  const updated = [...prev, newToast];
  return updated.slice(-MAX_TOASTS);
});

🔹 3. Add Types (Success / Error / Info)

const TYPE_STYLES = {
  success: "green",
  error: "red",
  info: "blue",
};

🔹 4. Add Position Support

position: "top" | "bottom"

Use:

  • react-native-reanimated

  • or LayoutAnimation

This is what makes your implementation stand out in interviews.


🧩 Common Mistakes to Avoid

  • ❌ Using useRef for UI state

  • ❌ Mutating arrays directly

  • ❌ Single boolean toast

  • ❌ Global timers instead of per-toast timers

  • ❌ No unique IDs


🧠 Final Thought

A toast system seems simple — but it's a great test of:

  • state management

  • component architecture

  • UI decoupling

  • lifecycle handling

If you can build this cleanly, you’re already thinking like a senior engineer.