RORK LABEN
MAX — Rork MaxがネイティブSwiftアプリを生成。iPhone・iPad・Watch・TV・Vision Pro・iMessageに対応しますNATIVE — AR/LiDARスキャン、Metalの3Dゲーム、ウィジェット、Live Activities、Core MLまで踏み込めますFUNDING — Rorkがa16zから$2.8Mを調達。月間74.3万訪問・成長率85%と勢いがありますPRICING — 無料で開始でき、有料プランは月額$25から利用できますFLOW — アイデアを英語で説明すると動くコードを生成。共有リンク発行やiOS/Androidビルドに対応しますCOMPARE — 従来のRorkはExpo/React Nativeでクロスプラットフォーム。用途で使い分けられますMAX — Rork MaxがネイティブSwiftアプリを生成。iPhone・iPad・Watch・TV・Vision Pro・iMessageに対応しますNATIVE — AR/LiDARスキャン、Metalの3Dゲーム、ウィジェット、Live Activities、Core MLまで踏み込めますFUNDING — Rorkがa16zから$2.8Mを調達。月間74.3万訪問・成長率85%と勢いがありますPRICING — 無料で開始でき、有料プランは月額$25から利用できますFLOW — アイデアを英語で説明すると動くコードを生成。共有リンク発行やiOS/Androidビルドに対応しますCOMPARE — 従来のRorkはExpo/React Nativeでクロスプラットフォーム。用途で使い分けられます
記事一覧/アプリ開発
アプリ開発/2026-06-27中級

「本当に削除しますか?」を出す前に — 取り消せる削除という選択肢

リストの項目を消すたびに確認ダイアログを出すと、ユーザーは反射的にOKを押すようになります。Rork(Expo)アプリで「取り消せる削除」を実装し、確認を出すべき場面と出さない場面を切り分ける設計ノートです。

Rork460Expo114UX設計5React Native186個人開発162

保存した壁紙を一覧から消そうとしたら「本当に削除しますか?」が出て、考えるより先に「OK」を押していた——自分のアプリを操作していて、そんな自分に気づいたことがあります。確認ダイアログは安全装置のつもりで置いたのに、毎回出てくるせいで、ユーザーは中身を読まずにボタンを押す癖がつきます。これでは安全装置として機能していません。

リストから項目を1つ消すような、頻繁で、かつ取り返しのつく操作には、確認ダイアログよりも「いったん消して、数秒だけ取り消せるようにする」ほうが合っています。個人開発で壁紙や癒し系のアプリを長く運用してきた私自身、確認を減らしてこの方式に寄せてから、削除まわりの戸惑いの声が目に見えて減りました。以下では、Rork(Expo)で作ったアプリにこの「取り消せる削除」を組み込む具体的な手順と、確認ダイアログを残すべき境界線を整理します。

確認ダイアログが向かない操作、向く操作

まず線引きをはっきりさせます。判断軸は「その操作はやり直せるか」と「どれくらいの頻度で起きるか」の2つです。

操作の例頻度取り返し向く手段
お気に入りから1枚外す高いつく取り消せる削除
メモ・履歴を1件消す高いつく取り消せる削除
アカウントを削除するごく稀つかない確認ダイアログ(+再入力)
課金・送信を確定するつかない確認ダイアログ
全データを初期化するごく稀つかない確認ダイアログ(+文言入力)

頻繁に起きて、やり直せる操作に確認を出すほど、ユーザーは「OKを押す」動作を学習します。逆に、取り返しのつかない操作にこそ、立ち止まらせる確認を集中させるべきです。確認を減らすことは、残した確認の重みを上げることでもあります。

「取り消せる削除」の仕組み

実装の肝は、画面から消すタイミングと、データを本当に消すタイミングをずらすことです。

  1. ユーザーが削除する → その項目を画面の一覧から即座に隠す(消えたように見える)
  2. 同時に「元に戻す」付きのトーストを数秒表示する
  3. 制限時間内に「元に戻す」が押されたら、隠していた項目を戻す
  4. 押されなければ、そこで初めてストレージから本当に削除する

つまり、削除は「保留中の削除」というバッファにいったん入り、時間切れで確定します。ユーザーから見れば即座に消え、しかも数秒の猶予がある、という二つの良さが両立します。

Expo での実装

保留中の削除を管理するカスタムフックを作ります。react-native 標準だけで動き、追加ライブラリは要りません。

// 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
) {
  // 保留中のID(画面からは隠すが、まだ消していない)
  const [pendingIds, setPendingIds] = useState<Set<string>>(new Set());
  const timers = useRef<PendingMap>({});
 
  // 実際に確定削除する
  const commit = useCallback(
    (id: string) => {
      delete timers.current[id];
      setPendingIds((prev) => {
        const next = new Set(prev);
        next.delete(id);
        return next;
      });
      commitDelete(id);
    },
    [commitDelete]
  );
 
  // 削除を予約(画面からは即座に消える)
  const remove = useCallback(
    (id: string) => {
      setPendingIds((prev) => new Set(prev).add(id));
      timers.current[id] = setTimeout(() => commit(id), windowMs);
    },
    [commit, windowMs]
  );
 
  // 取り消し(タイマーを止めて戻す)
  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;
    });
  }, []);
 
  // 画面に出すのは「保留中でない」項目だけ
  const visibleItems = items.filter((it) => !pendingIds.has(it.id));
 
  // アンマウント時、保留中はすべて確定してから消える
  useEffect(() => {
    return () => {
      Object.keys(timers.current).forEach((id) => {
        clearTimeout(timers.current[id]);
        commitDelete(id);
      });
    };
  }, [commitDelete]);
 
  return { visibleItems, remove, undo, pendingIds };
}

ポイントは、画面に表示する visibleItems を「保留中でないものだけ」に絞っていることです。状態は1か所(pendingIds)に集約しているので、リストとトーストが食い違う心配がありません。

呼び出し側はこうなります。

// 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, // ここで AsyncStorage 等から実削除
    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件削除しました</Text>
          <TouchableOpacity onPress={handleUndo}>
            <Text style={{ color: "#4da3ff", fontWeight: "600" }}>元に戻す</Text>
          </TouchableOpacity>
        </View>
      )}
    </View>
  );
}

トーストの自動消滅は、削除確定の windowMs と揃えておくと挙動が読みやすくなります。ここでは簡単のため lastDeleted を直接管理していますが、トーストにも同じ秒数のタイマーを持たせて、時間切れで setLastDeleted(null) するときれいです。

見落としやすい「アプリが裏に回る」問題

ここがいちばんつまずきやすいところです。削除を予約した直後にユーザーがホーム画面に戻ると、setTimeout はOSの都合で止まったり遅れたりします。すると「消えたはずの項目が次の起動で生き返る」という、いちばん不信感を生む挙動になります。

対策は、アプリが裏に回る瞬間に、保留中の削除をすべて確定してしまうことです。猶予はフォアグラウンドにいる間だけ、と割り切ります。

import { AppState } from "react-native";
 
// useUndoableDelete 内に追記
useEffect(() => {
  const sub = AppState.addEventListener("change", (state) => {
    if (state !== "active") {
      // 裏に回るときは保留分をすべて確定
      Object.keys(timers.current).forEach((id) => {
        clearTimeout(timers.current[id]);
        commit(id);
      });
    }
  });
  return () => sub.remove();
}, [commit]);

これで「取り消せるのはアプリを見ている間だけ」という一貫したルールになり、復活バグも防げます。上のアンマウント時の確定処理と合わせて、保留が宙に浮く経路をすべて塞いでおきます。

Rork に作らせるときのプロンプトのコツ

Rork に最初から作らせる場合、「削除ボタンに確認ダイアログを付けて」と頼むと、たいてい Alert.alert の確認が入ります。そうではなく、挙動を言葉で指定します。たとえば「リストの項目を削除したら、すぐ一覧から消して、画面下に『元に戻す』ボタン付きの通知を5秒出して。5秒以内に押さなければ確定削除、押したら戻す」のように、確認ではなく取り消しで設計してほしいと伝えると、意図した形に近づきます。生成されたコードでも、上で挙げた「裏に回ったとき」の確定処理は抜けがちなので、そこだけは自分で足すつもりでいるとよいです。

確認ダイアログは万能の安全装置ではなく、出しすぎると効き目が落ちる道具です。次に削除ボタンを設計するときは、「これは取り返しがつくか」を一度自分に問いかけてみてください。つくのなら、確認で止めるより、消してから戻せるようにするほうが、たいていの場合ユーザーに優しい設計になります。

関連して、削除後に一覧が空になったときの見せ方はRork アプリの空状態を設計しきるが、削除ボタンを含む画面設計の引き出しはRork アプリの UX デザインパターンが参考になります。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

アプリ開発2026-06-27
アラビア語に切り替えても画面が鏡像化されない — Rork(Expo)アプリのRTL対応と再起動の罠
Rork が生成した Expo アプリにアラビア語を足したのに、画面が左右反転せず戻るボタンが逆側に残る——その原因は I18nManager.forceRTL が再起動を要求することにあります。expo-localization での方向判定、Updates.reloadAsync での確実な反映、marginStart への置き換え、矢印だけの鏡像化まで、動くコードで RTL 対応の本番設計を示します。
アプリ開発2026-06-03
壁紙アプリ 6 本のオンボーディングを共通化して、1 ヶ月の初日定着を見た所感
Rork で骨格を作った壁紙アプリ 6 本のオンボーディングを 1 つの設定駆動コンポーネントに統一し、1 ヶ月の初日継続率とプッシュ許諾率がどう動いたかを、個人開発の現場目線で淡々と記録した実運用メモです。
開発ツール2026-06-22
Rork(Expo)のトーストは、重なっても読み上げられても崩れない設計にする
Rork が生成した React Native アプリにトースト通知を足すとき、単純な実装は『同時に2つ出ると重なる』『スクリーンリーダーに無視される』『ノッチやホームインジケータに隠れる』の3点で破綻します。ルート1か所に置くキュー設計、本体の再レンダーから切り離すアニメーション、AccessibilityInfo による読み上げ、セーフエリア対応までを動くコードで示します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →