BluePeriod Docs
開発トラブルシューティング

Reactにおける安全な自動保存機能の実装ベストプラクティス

Reactアプリケーションにおける自動保存の設計と実装指針

Reactにおける安全な自動保存機能の実装ベストプラクティス

日付: 2025-11-08 更新: 2026-01-06 (現在の実装に合わせて修正)

概要

このドキュメントは、Reactコンポーネント(特に AutoSaveManager.tsx のようなヘッドレスコンポーネント)で、非同期の自動保存機能を実装する際のベストプラクティスについて概説します。

過去に、「Can't perform a React state update on a component that hasn't mounted yet.(マウントされていないコンポーネントに対してReactの状態更新はできません)」というエラーが頻発しました。このエラーは、コンポーネントが画面から消えた(アンマウントされた)後にも、非同期処理(この場合はデバウンスされた保存処理)が完了し、状態を更新しようとすることで発生します。

この問題を回避し、堅牢な自動保存機能を実装するための指針を以下に示します。


課題: アンマウント後の状態更新

自動保存機能では、ユーザーの入力が終わってから一定時間後に保存処理を実行する「デバウンス」という手法がよく使われます。

推奨されるパターン: isProjectDirtyAtom の監視

現在の AutoSaveManager.tsx では、プロジェクトの未保存状態を表す isProjectDirtyAtom を監視し、変更がある場合にのみ2秒のデバウンス後に保存を実行しています。

安全なコード (AutoSaveManager.tsx の現在の実装):

import { useEffect, useRef } from 'react';
import { useAtomValue, useSetAtom } from 'jotai';
import { isProjectDirtyAtom } from '@/stores/uiStateAtoms';
import { saveProjectAtom } from '@/stores/projectPersistenceAtoms';

const AUTOSAVE_DELAY = 2000; // 2秒

export const AutoSaveManager = () => {
    const isDirty = useAtomValue(isProjectDirtyAtom);
    const saveProject = useSetAtom(saveProjectAtom);
    const timeoutRef = useRef<NodeJS.Timeout | null>(null);

    useEffect(() => {
        // 既存のタイマーをクリア
        if (timeoutRef.current) {
            clearTimeout(timeoutRef.current);
        }

        // 変更がある場合のみタイマーを開始
        if (isDirty) {
            console.log('[AutoSaveManager] Project is dirty, scheduling auto-save...');
            timeoutRef.current = setTimeout(() => {
                console.log('[AutoSaveManager] Auto-saving project...');
                saveProject();
            }, AUTOSAVE_DELAY);
        }

        // ★★★ クリーンアップ関数 ★★★
        return () => {
            if (timeoutRef.current) {
                clearTimeout(timeoutRef.current);
            }
        };
    }, [isDirty, saveProject]);

    return null;
};

実装のポイント

  1. useAtomValue の使用: isProjectDirtyAtom の値のみを監視し、不要な再レンダリングを防ぎます。

  2. useSetAtom の使用: saveProjectAtom のsetterのみを取得し、コンポーネントの依存関係を最小限にします。

  3. useRef によるタイマー管理: timeoutRef でタイマーIDを保持し、クリーンアップ関数で確実にキャンセルします。

  4. 条件付き保存: isDirtytrue の場合のみタイマーを開始し、無駄な保存処理を回避します。


なぜこれが安全なのか?

この実装では、コンポーネントがアンマウントされる直前にクリーンアップ関数が clearTimeout(timeoutRef.current) を呼び出します。これにより、保留されていた saveProject() の呼び出しが確実に取り消されるため、アンマウント後の状態更新エラーは決して発生しません。


結論

AutoSaveManager や同様の機能を持つコンポーネントを将来的に実装・修正する際は、非同期処理、特にデバウンスを伴う副作用を扱う場合、必ず useEffect のクリーンアップ関数を用いて、保留中の処理をキャンセルする実装パターンに従ってください。これにより、アプリケーションの安定性が向上し、予測不能なライフサイクル関連のバグを防ぐことができます。

On this page