RORK LABJP
MAX — Rork Max generates native Swift apps across iPhone, iPad, Watch, TV, Vision Pro, and iMessageNATIVE — It reaches AR/LiDAR scanning, Metal 3D games, widgets, Live Activities, and on-device Core MLFUNDING — Rork raised $2.8M from a16z, with 743K monthly visits and 85% growthPRICING — It's free to start, with paid plans beginning at $25 per monthFLOW — Describe your idea in plain English to get working code, a shareable test link, and iOS/Android buildsCOMPARE — The original Rork builds cross-platform apps on Expo/React Native; choose the right tool per goalMAX — Rork Max generates native Swift apps across iPhone, iPad, Watch, TV, Vision Pro, and iMessageNATIVE — It reaches AR/LiDAR scanning, Metal 3D games, widgets, Live Activities, and on-device Core MLFUNDING — Rork raised $2.8M from a16z, with 743K monthly visits and 85% growthPRICING — It's free to start, with paid plans beginning at $25 per monthFLOW — Describe your idea in plain English to get working code, a shareable test link, and iOS/Android buildsCOMPARE — The original Rork builds cross-platform apps on Expo/React Native; choose the right tool per goal
Articles/App Dev
App Dev/2026-06-27Intermediate

Before You Ask 'Are You Sure?' — Consider an Undoable Delete

Showing a confirmation dialog every time someone removes a list item trains them to tap OK without reading. Here is how to build an undoable delete in a Rork (Expo) app, and where confirmation dialogs still belong.

Rork461Expo114UX Design7React Native186Indie Dev32

I once caught myself tapping "OK" before I had even read the words, while removing a saved wallpaper from a list in my own app. The "Are you sure you want to delete this?" dialog was meant to be a safety net, but because it appeared every single time, I had trained myself to dismiss it on reflex. A safety net that nobody reads is not really protecting anyone.

For actions that happen often and can be reversed — like removing one item from a list — an undoable delete fits better than a confirmation dialog: remove it now, and let the person take it back for a few seconds. As an indie developer who has run wallpaper and calming-themed apps for years, I saw delete-related questions drop noticeably once I leaned on this pattern instead of confirmations. This article walks through adding an undoable delete to an app built with Rork (Expo), and where you should still keep a confirmation dialog.

Where confirmations fit, and where they don't

Let's draw the line clearly. Two questions decide it: can the action be reversed, and how often does it happen?

Example actionFrequencyReversible?Better fit
Remove one favoriteHighYesUndoable delete
Delete one note / history itemHighYesUndoable delete
Delete accountVery rareNoConfirmation (+ retype)
Confirm a payment / sendRareNoConfirmation
Reset all dataVery rareNoConfirmation (+ type to confirm)

The more you put a confirmation in front of a frequent, reversible action, the more you teach people to "just tap OK." Save the pauses for the actions that truly cannot be taken back. Reducing confirmations is also a way to raise the weight of the ones you keep.

How an undoable delete works

The key is to separate when the item disappears from the screen from when the data is actually deleted.

  1. The person deletes an item, and it is hidden from the list immediately (it looks gone).
  2. At the same time, a toast with an "Undo" button appears for a few seconds.
  3. If "Undo" is tapped within the window, the hidden item comes back.
  4. If it isn't, only then is the item really removed from storage.

In other words, a delete first goes into a "pending delete" buffer and is committed when the window expires. From the user's point of view, it vanishes instantly and there is still a grace period — you get both.

Implementing it in Expo

Let's build a custom hook that manages pending deletes. It runs on plain react-native with no extra libraries.

// useUndoableDelete.ts
import { useCallback, useEffect, useRef, useState } from "react";
 
type PendingMap = Record<string, ReturnType<typeof setTimeout>>;
 
export function useUndoableDelete<T extends { id: string }>(
  items: T[],
  commitDelete: (id: string) => Promise<void> | void,
  windowMs = 5000
) {
  // Pending IDs: hidden from the screen, but not deleted yet
  const [pendingIds, setPendingIds] = useState<Set<string>>(new Set());
  const timers = useRef<PendingMap>({});
 
  // Actually commit the deletion
  const commit = useCallback(
    (id: string) => {
      delete timers.current[id];
      setPendingIds((prev) => {
        const next = new Set(prev);
        next.delete(id);
        return next;
      });
      commitDelete(id);
    },
    [commitDelete]
  );
 
  // Schedule a delete (disappears from the screen immediately)
  const remove = useCallback(
    (id: string) => {
      setPendingIds((prev) => new Set(prev).add(id));
      timers.current[id] = setTimeout(() => commit(id), windowMs);
    },
    [commit, windowMs]
  );
 
  // Undo (cancel the timer and restore)
  const undo = useCallback((id: string) => {
    const t = timers.current[id];
    if (t) clearTimeout(t);
    delete timers.current[id];
    setPendingIds((prev) => {
      const next = new Set(prev);
      next.delete(id);
      return next;
    });
  }, []);
 
  // Render only the items that are NOT pending
  const visibleItems = items.filter((it) => !pendingIds.has(it.id));
 
  // On unmount, commit everything pending before leaving
  useEffect(() => {
    return () => {
      Object.keys(timers.current).forEach((id) => {
        clearTimeout(timers.current[id]);
        commitDelete(id);
      });
    };
  }, [commitDelete]);
 
  return { visibleItems, remove, undo, pendingIds };
}

The important part is that visibleItems is narrowed to "everything that is not pending." All the state lives in one place (pendingIds), so the list and the toast can never disagree.

The calling side looks like this:

// FavoritesScreen.tsx
import { useState } from "react";
import { FlatList, Text, TouchableOpacity, View } from "react-native";
import { useUndoableDelete } from "./useUndoableDelete";
 
export function FavoritesScreen({ data, onDeleteFromStorage }) {
  const [lastDeleted, setLastDeleted] = useState<string | null>(null);
 
  const { visibleItems, remove, undo } = useUndoableDelete(
    data,
    onDeleteFromStorage, // do the real delete here (AsyncStorage, etc.)
    5000
  );
 
  const handleDelete = (id: string) => {
    remove(id);
    setLastDeleted(id);
  };
 
  const handleUndo = () => {
    if (lastDeleted) undo(lastDeleted);
    setLastDeleted(null);
  };
 
  return (
    <View style={{ flex: 1 }}>
      <FlatList
        data={visibleItems}
        keyExtractor={(it) => it.id}
        renderItem={({ item }) => (
          <TouchableOpacity onPress={() => handleDelete(item.id)}>
            <Text style={{ padding: 16 }}>{item.title}</Text>
          </TouchableOpacity>
        )}
      />
      {lastDeleted && (
        <View
          style={{
            position: "absolute",
            bottom: 24,
            left: 16,
            right: 16,
            flexDirection: "row",
            justifyContent: "space-between",
            backgroundColor: "#222",
            padding: 14,
            borderRadius: 12,
          }}
        >
          <Text style={{ color: "#fff" }}>1 item deleted</Text>
          <TouchableOpacity onPress={handleUndo}>
            <Text style={{ color: "#4da3ff", fontWeight: "600" }}>Undo</Text>
          </TouchableOpacity>
        </View>
      )}
    </View>
  );
}

Keep the toast's auto-dismiss aligned with the windowMs used to commit the delete, and the behavior stays easy to reason about. For brevity I track lastDeleted directly here; in production, give the toast its own timer of the same length and call setLastDeleted(null) when it expires.

The easy-to-miss problem: the app goes to the background

This is where most people trip. If the person hits the home button right after scheduling a delete, the OS may pause or delay setTimeout. The result is the worst kind of behavior for trust: an item that "should be gone" comes back on the next launch.

The fix is to commit all pending deletes the moment the app leaves the foreground. Decide that the grace period only exists while the app is on screen.

import { AppState } from "react-native";
 
// add inside useUndoableDelete
useEffect(() => {
  const sub = AppState.addEventListener("change", (state) => {
    if (state !== "active") {
      // Commit everything pending when going to the background
      Object.keys(timers.current).forEach((id) => {
        clearTimeout(timers.current[id]);
        commit(id);
      });
    }
  });
  return () => sub.remove();
}, [commit]);

Now the rule is consistent — "you can undo only while you are looking at the app" — and the resurrection bug is gone. Together with the unmount commit above, this closes every path that could leave a delete dangling.

Prompting Rork to build it

If you ask Rork to "add a confirmation dialog to the delete button," you will usually get an Alert.alert confirmation. Instead, describe the behavior. Something like: "When a list item is deleted, remove it from the list right away and show a notification at the bottom with an Undo button for 5 seconds; commit the delete if Undo isn't tapped within 5 seconds, otherwise restore it." Telling it to design around undo rather than confirmation gets you much closer to what you want. Generated code tends to skip the background-commit step from above, so plan to add that one yourself.

A confirmation dialog is not a universal safety net; overused, it loses its effect. Next time you design a delete button, ask yourself once: can this be taken back? If it can, letting people delete and then restore is usually kinder than stopping them with a question.

On a related note, for how to present a list that becomes empty after deletion, see Designing the empty states of a Rork app, and for the broader screen-design toolkit around delete buttons, see UX design patterns for Rork apps.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

App Dev2026-06-27
Your Arabic Users See an Unmirrored Layout — RTL in a Rork (Expo) App and the Reload Trap
You added Arabic to a Rork-generated Expo app, but the screen never flips and the back button stays on the wrong side. The cause is that I18nManager.forceRTL requires a relaunch. This walks through detecting direction with expo-localization, applying it reliably with Updates.reloadAsync, swapping to marginStart, and mirroring only the arrows — all with working code.
App Dev2026-06-03
Unifying Onboarding Across Six Wallpaper Apps: What One Month of First-Day Retention Showed Me
I folded the onboarding flows of six wallpaper apps scaffolded with Rork into a single config-driven component and watched first-day retention and push opt-in for a month. Here is an honest, operational note on what moved and what didn't.
Dev Tools2026-06-22
Build a Toast System in Your Rork App That Survives Overlaps, Screen Readers, and Notches
When you add toast notifications to a React Native app generated by Rork, the naive version breaks in three places: two toasts overlap, screen readers stay silent, and the text hides under the notch or home indicator. This walks through a root-level queue, animations decoupled from your app tree, AccessibilityInfo announcements, and safe-area placement — all in working code.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →