Skip to main content
Deduplication prevents identical toasts from stacking up when triggered multiple times rapidly (e.g., from repeated button taps). Instead of creating multiple toasts, React Native Bread resets the timer and plays a feedback animation.

How deduplication works

When deduplication is enabled and a duplicate toast is detected:
  1. The existing toast’s timer is reset
  2. The toast content is updated (if using an id)
  3. A feedback animation plays:
    • Error toasts: Shake animation (400ms, from package/src/constants.ts:24)
    • Other toast types: Pulse animation (300ms, from package/src/constants.ts:23)
  4. The duplicate toast request returns the existing toast’s ID

Default behavior

Deduplication is enabled by default (from package/src/toast-store.ts:26):
deduplication: true
This means duplicate toasts are automatically prevented without any configuration.

Matching logic

React Native Bread uses two strategies to detect duplicates:

Without an ID

By default, a toast is considered a duplicate when it matches the front toast (the newest visible toast) by:
  • Title
  • Type
  • Description
From package/src/toast-store.ts:114-122:
const frontToast = this.state.visibleToasts.find(t => !t.isExiting);
const duplicate = frontToast &&
  frontToast.title === title &&
  frontToast.type === type &&
  frontToast.description === resolvedDescription
    ? frontToast
    : undefined;

With an ID

When you provide an id option, matching becomes more stable and content can be updated:
const duplicate = deduplicationId
  ? this.state.visibleToasts.find(t => !t.isExiting && t.options?.id === deduplicationId)
  : // falls back to title+type+description matching
From package/src/toast-store.ts:113-122.

Basic deduplication example

import { toast } from 'react-native-bread';
import { Pressable, Text } from 'react-native';

function LikeButton() {
  const handleLike = () => {
    // Rapid taps won't create multiple toasts
    toast.success('Liked!');
  };

  return (
    <Pressable onPress={handleLike}>
      <Text>Like</Text>
    </Pressable>
  );
}
Behavior: No matter how many times the button is tapped, only one toast is visible. Each tap resets the timer and plays a pulse animation.

Using IDs for stable matching

IDs are useful when you want to update the toast content while preventing duplicates:
function SaveButton() {
  const [itemsSaved, setItemsSaved] = useState(0);

  const handleSave = () => {
    const count = itemsSaved + 1;
    setItemsSaved(count);
    
    // Same ID = updates content instead of creating new toast
    toast.success(`Saved ${count} items`, {
      deduplication: true,
      id: 'save-action',
    });
  };

  return (
    <Pressable onPress={handleSave}>
      <Text>Save</Text>
    </Pressable>
  );
}
Behavior: The first tap shows “Saved 1 items”. The second tap updates it to “Saved 2 items” and resets the timer. The toast content changes, but no new toast is created. From package/src/toast-store.ts:124-133:
if (duplicate) {
  this.updateToast(duplicate.id, {
    title,
    description: resolvedDescription,
    type,
    deduplicatedAt: Date.now(),
    duration: actualDuration,
  });
  return duplicate.id;
}

Animation feedback

Deduplication triggers different animations based on toast type:

Pulse animation (non-error toasts)

Success, info, and loading toasts play a subtle scale bump:
  • Duration: 300ms (from package/src/constants.ts:23)
  • Effect: Slight scale increase then back to normal
  • Purpose: Provides gentle feedback that the action was registered
// These trigger pulse animation when deduplicated
toast.success('Copied!');
toast.info('Notification received');

Shake animation (error toasts)

Error toasts play a shake effect to draw more attention:
  • Duration: 400ms (from package/src/constants.ts:24)
  • Effect: Horizontal shake motion
  • Purpose: Emphasizes that the error is still present
// This triggers shake animation when deduplicated
toast.error('Network error');

Global configuration

Disable deduplication globally

import { BreadLoaf } from 'react-native-bread';

function App() {
  return (
    <>
      <NavigationContainer>...</NavigationContainer>
      <BreadLoaf
        config={{
          deduplication: false, // All toasts will stack, even duplicates
        }}
      />
    </>
  );
}
With this configuration, every toast.success('Liked!') call creates a new toast, even if called rapidly.

Per-toast configuration

You can override the global deduplication setting for individual toasts:

Opt out of deduplication

// Disable deduplication for this specific toast
toast.info('New message', { 
  deduplication: false 
});

Explicitly enable deduplication

// Enable deduplication even if disabled globally
toast.success('Liked!', { 
  deduplication: true 
});
From package/src/types.ts:103-104:
/** Enable deduplication for this toast (overrides global config). 
    Plays a pulse animation for non-error toasts or a shake for errors. 
    Use with `id` for stable matching across different content. */
deduplication?: boolean;

Loading toasts and deduplication

Loading toasts never participate in deduplication, even if the feature is enabled globally. From package/src/toast-store.ts:111:
const shouldDedup = type !== "loading" && (options?.deduplication ?? this.theme.deduplication);
This ensures that multiple async operations each get their own loading indicator:
// Each promise gets its own loading toast
toast.promise(fetchUser(), {
  loading: 'Loading user...',
  success: 'User loaded',
  error: 'Failed to load user',
});

toast.promise(fetchPosts(), {
  loading: 'Loading posts...',
  success: 'Posts loaded',
  error: 'Failed to load posts',
});

Real-world examples

Example 1: Form submission

function SubmitButton() {
  const handleSubmit = async () => {
    // User might double-tap the submit button
    const result = await toast.promise(submitForm(), {
      loading: 'Submitting...', // Loading never deduplicates
      success: { 
        title: 'Submitted!',
        // Success deduplicates by default
      },
      error: (err) => ({ 
        title: 'Submission failed',
        description: err.message,
      }),
    });
  };

  return <Button onPress={handleSubmit}>Submit</Button>;
}

Example 2: Like counter with ID

function LikeButton({ postId }: { postId: string }) {
  const handleLike = async () => {
    await likePost(postId);
    
    // Using postId ensures only one toast per post
    // Content updates with each like
    toast.success('Liked!', {
      id: `like-${postId}`,
      deduplication: true,
    });
  };

  return <Button onPress={handleLike}>Like</Button>;
}

Example 3: Notification system

function NotificationHandler() {
  useEffect(() => {
    const subscription = notificationService.subscribe((notification) => {
      // Each unique notification gets its own toast
      toast.info(notification.title, {
        description: notification.body,
        id: notification.id, // Use notification ID to prevent duplicates
        deduplication: true,
      });
    });

    return () => subscription.unsubscribe();
  }, []);

  return null;
}

Example 4: Clipboard operations

function CopyButton({ text }: { text: string }) {
  const handleCopy = async () => {
    await Clipboard.setString(text);
    
    // Quick feedback, but prevent spam if user rapidly clicks
    toast.success('Copied to clipboard', {
      duration: 2000,
      deduplication: true,
    });
  };

  return <Button onPress={handleCopy}>Copy</Button>;
}

When to use deduplication

Enable deduplication when:

  • Users might trigger the same action multiple times rapidly
  • You want to update existing toast content instead of stacking
  • Preventing visual clutter is important
  • The action provides confirmation feedback (save, copy, like, etc.)

Disable deduplication when:

  • Each toast represents a distinct event (e.g., incoming messages)
  • You want to show a count or list of similar events
  • The content varies significantly even with the same title
  • Debugging toast behavior during development

Type definitions

From package/src/types.ts:74:
/** When true, duplicate toasts reset the timer and play a feedback animation. 
    Matches by title+type+description, or by `id` if provided. (default: true) */
deduplication: boolean;
From package/src/types.ts:79:
/** Stable key for deduplication. When set, toasts with the same `id` deduplicate 
    and update the existing toast's content. Without an `id`, matching falls back 
    to title+type+description against the front toast. */
id?: string;
From package/src/types.ts:124:
deduplicatedAt?: number; // Timestamp of last deduplication event

Build docs developers (and LLMs) love