◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/開発ツール
⬡ 開発ツール/2026-07-12上級

取り消しを押しても戻らない——編集履歴を安全に巻き戻すUndo/Redoの設計

状態を丸ごと保存するUndoは、数十手でメモリを圧迫します。コマンド履歴とスナップショットを使い分け、微調整の合体・上限・セッション跨ぎの永続化まで備えたUndo/Redoを、動くTypeScriptで設計します。

undo-redoreact-native12expo12architecture6state-management

✦ プレミアム記事

壁紙を合成するアプリで、ユーザーが背景の色を少しずつ調整し、ステッカーを10個ほど並べ、最後にうっかり全消去のボタンに触れてしまった——というレビューをいただいたことがあります。慌てて取り消しを押したものの、戻ってきたのは全消去の直前ではなく、なぜか5手前の中途半端な状態でした。「取り消しが効かないなら、もう触るのが怖い」。短い一文でしたが、胸の奥がひやりとしました。

私自身、個人開発で編集操作のあるアプリをいくつか運用しています。Undo/Redo は「あればいい機能」ではなく、ユーザーが安心して手を動かせるかどうかを決める土台だと考えるようになりました。そして実装してみると、素朴な作り方ほど後で落とし穴になることも、身をもって知りました。

React Native(Expo)で編集系アプリの Undo/Redo を、長く運用に耐える形で設計していきます。動く TypeScript を軸に、メモリの圧迫・微調整の扱い・アプリ再起動後の履歴保持という、本番運用で必ずぶつかる三つの壁を越えていきます。

なぜ「状態を丸ごと保存する」Undoは破綻するのか

最初に思いつくのは、操作のたびに状態全体をコピーして配列に積む方法です。取り消しは一つ前のコピーに戻すだけ。実装は数十行で済みます。

ところが編集系アプリでは、この素朴さがそのまま落とし穴になります。壁紙の合成状態が、画像のトリミング情報・数十個のレイヤー・フィルタのパラメータを含むとしましょう。一つの状態が仮に80KBだとして、ユーザーが100手編集すれば、履歴だけで8MBを抱えます。スライダーを動かすたびに1手と数えれば、数分の編集で軽く超えていきます。

もう一つの問題は「何が変わったのか」が履歴に残らないことです。取り消しボタンに「色の変更を取り消す」と出したくても、丸ごとのコピーからは差分が読み取れません。

方式メモリ差分の意味向く場面
スナップショット(状態全体)大きい(状態サイズ×手数)残らない状態が小さい・操作が少ない
コマンド(操作の差分)小さい(差分サイズ×手数)残る(ラベル化できる)編集が続く・履歴が深い

私は、編集が長く続くアプリではコマンド方式を基本に据えることを推奨します。ただしスナップショットも捨てません。後述するセッション跨ぎの永続化では、むしろスナップショットが効いてきます。両者は敵対しません。役割が違うだけです。

コマンドを最小単位にする

コマンドとは「実行する(do)」と「取り消す(undo)」の対を持つ、操作の一単位です。差分だけを抱えるので軽く、ラベルを添えられます。まずは型で輪郭を決めます。

// 一手ぶんの操作。do と undo は対称であること。
export interface Command {
  readonly label: string;          // 「色の変更」など、UIに出せる名前
  do(): void;                      // 前へ進める
  undo(): void;                    // 一手戻す
  // 直前の同種コマンドと合体できるなら、合体後のコマンドを返す
  coalesceWith?(prev: Command): Command | null;
}

具体例として、レイヤーの位置を動かすコマンドを書きます。差分(移動前と移動後の座標)だけを保持している点に注目してください。

export function moveLayerCommand(
  store: EditorStore,
  layerId: string,
  from: Point,
  to: Point,
): Command {
  return {
    label: "レイヤーの移動",
    do: () => store.setLayerPosition(layerId, to),
    undo: () => store.setLayerPosition(layerId, from),
    coalesceWith: (prev) => {
      // 同じレイヤーを連続で動かしているなら、始点を引き継いで一手に束ねる
      const p = prev as ReturnType<typeof moveLayerCommand>;
      if (p.label !== "レイヤーの移動") return null;
      return moveLayerCommand(store, layerId, (p as any)._from ?? from, to);
    },
  };
}

ここで大切なのは、コマンドが状態の「置き場所」を知っているものの、状態そのものを丸ごと抱えないことです。抱えるのは変化した値だけ。これがメモリを軽く保つ肝になります。

✦

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

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦状態全体を積むUndoがメモリを食い潰す理由と、コマンド差分で軽くする実装
✦連続する微調整を一手にまとめる合体(coalescing)の判定と、境界の見極め
✦アプリがバックグラウンドで破棄されても履歴を失わない、上限付き永続化の設計
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

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

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

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

関連記事

⬡ 開発ツール2026-05-20
iOS の 0x8badf00d ウォッチドッグ強制終了に Rork アプリが落ちるときの対処
Rork で作った iOS アプリが起動直後に消える。クラッシュログに 0x8badf00d。Apple のウォッチドッグが React Native の重い初期化を切ったときの正しい直し方を、私の運用中アプリで実際に効いた手順で説明します。
⬡ 開発ツール2026-04-24
Rork アプリのバイナリサイズが 150MB を超えた時に切り分ける 5 つの要因と実践的な削減手順
Rork で生成したアプリが 180MB まで膨らみ、App Store Connect の警告で止まった経験から、原因を 5 要素に分解して 69MB まで落とすまでの手順を、実測値と失敗した箇所も含めて共有します。
⬡ 開発ツール2026-10-15
EXPO_PUBLIC_ の値が、開発では読めて本番だけ undefined になる書き方を、変換結果で見分けます
API の URL が開発中は読めるのに、ビルドした途端に undefined になる。原因は process.env の書き方でした。babel-preset-expo の変換結果を開発・本番で並べ、置き換わる書き方と残ってしまう書き方を表にしました。