AIにRemixを任せる前に知っておくこと ― Reactの歴史がSSRで牙を剥くとき

RemixでFlashMessageを実装したとき、useEffect のタイマーが10秒待っても発火しませんでした。
原因はコードの論理ではなく、親コンポーネントの再レンダリングのたびにReactがコンポーネントを再マウントしていたことです。key={loaderData.flashMessage} を追加するだけで解決しました。
Remixでこんなことありませんか?
useEffectの依存配列を何度直しても、タイマーが正しく発火しない。- AIに確認すると「コードは論理的に正しい」と言われるが、E2Eテストは失敗し続ける。
- クリーンアップ関数も書いているのに、コンポーネントが意図しないタイミングで再マウントされている。
この記事をお勧めしない人
- AIが生成したコードは、基本的にそのまま動くと信じている人。
- ReactのuseEffectは、どの環境でも同じように動作すると思っている人。
- 「なぜ動かないか」より「どう直すか」だけに興味がある人。
もし一つでも当てはまらないなら、読み進める価値があるかもしれません。
SSR環境でuseEffectを使い続けると
- AIはSPA時代の学習データに基づくため、ハイドレーション後のコンポーネント再マウントという挙動を前提に提案しない。
- 依存配列の調整→クリーンアップ関数追加→別実装試行というループを繰り返し、根本原因にたどり着かない。
- 「正しいコードなのに動かない」という状態が続き、SSR環境固有の前提が見えないまま時間が溶ける。
`key` プロップによる同一性の保証
- この記事を読めば、Reactがコンポーネントの同一性をどう判断するかという構造が理解でき、SSRで
keyをどう使うかの設計図が手に入る。 - 具体的には、
key={loaderData.flashMessage}で「同じメッセージ = 同一コンポーネント」とReactに認識させる実装パターンが手に入る。 - この方法は、このブログのFlashMessage実装で実証済みで、E2Eテストの失敗をゼロにした。
私も同じでした
このブログでFlashMessageを実装したとき、useEffect でタイマーを5秒に設定してもE2Eテストが Timeout exceeded で失敗し続けました。AIは「コードは正しい」と繰り返しました。親ルートの再レンダリングのたびにコンポーネントが再マウントされタイマーがリセットされていることに気づき、key={loaderData.flashMessage} を追加したところ一発で解決しました。
概要
AIが書いた「論理的に完璧なコード」が動かない。この不気味な現象の背後には、 AIの学習データ(Reactの歴史) と 実行環境(SSR/Remix) の乖離があります。
この記事では、FlashMessageのタイマーが動かない具体的な事例を通じて、その構造を解剖し、AI時代の「延命医」としての作法を提示します。
発生環境
- フレームワーク : Remix v2(SSR)
- ホスティング : Cloudflare Pages/Workers
- 問題のコンポーネント : FlashMessage(自動消去機能)
第1層:表面的な事象 ― 「正しいコード」が動かない
FlashMessageコンポーネントを実装しました。5秒後に自動で消える、よくある機能です。
Reactのフックを使ってタイマーを設定し、クリーンアップ関数で解除している。依存配列も最小限に絞っている。 このコードは論理的に完璧です。
しかし、E2Eテストは失敗します。
期待: 5秒後に消える
現実: 10秒待っても消えないAIに聞いても「コードは正しい」と言う。依存配列を修正しても動かない。 ここに、AI時代特有の不気味さが現れます。
第2層:認識の転換 ― AIの「学習データ」と「実行環境」の乖離
AIが学習した「Reactの歴史」
AIはReactの膨大なコードベースとドキュメントを学習しています。そこに蓄積されているのは、 SPA(シングルページアプリケーション)時代の常識 です。
- useEffectはコンポーネントのマウント時に一度だけ実行される
- 依存配列を正しく書けば、意図した通りに再実行される
- タイマーはクリーンアップ関数で適切に解除される
これが「教科書の正解」であり、AIが信じている世界です。
あなたの戦場「SSR(Remix)」
しかし、Remixで開発しているあなたの戦場は異なります。
- サーバーでHTMLを生成する(SSR)
- クライアントでそのHTMLを「引き継ぐ」(ハイドレーション)
- この二重構造が、教科書の正解を裏切る
「歴史(過去の正解)」が「最新(現在の戦場)」の足を引っ張っている という構図を認識する必要があります。
第3層:構造的な解剖 ― ハイドレーションという名の「アイデンティティ不安」
Reactの「自己防衛」
ReactはSSRにおいて、サーバーから引き継いだHTMLが 本当に自分の分身かどうか、常に疑っています 。
これは設計上の正しい判断です。サーバーとクライアントで状態が食い違っていたら、バグの温床になるからです。
しかし、この「慎重さ」が問題を引き起こします。
「殺して作り直す」メカニズム
親コンポーネントが再レンダリングされるという些細な刺激で、Reactは子コンポーネントの同一性を疑います。
親の再レンダリング
↓
Reactが「この子は本当に同一人物か?」と疑う
↓
確信が持てないので、殺して作り直す(再マウント)
↓
useEffectが再実行、タイマーが0秒にリセット
↓
永遠に5秒に到達しないこの「システム側の自己防衛的な挙動」が、タイマーを永遠に0秒へ巻き戻し続ける真犯人です。
なぜAIはこれを指摘できないのか
AIは「コードの論理」を見ています。しかし、この問題は「コードの論理」ではなく「実行環境の構造」にあります。
- AIの視点: 「useEffectの依存配列は正しい」「クリーンアップも書いてある」
- 現実の問題: 「そもそもコンポーネントが何度も殺されている」
AIは論理には強いが、文脈(歴史や環境)の衝突には疎い。 これがAI時代に人間が理解すべき最も重要なことです。
では、この「アイデンティティ不安」をどう解決するのか。古い道具(keyプロップ)を新しい意味で使う建築家的アプローチ、具体的なコード実装、依存配列の最小化テクニック、そしてCLAUDE.mdへの知見の蓄積方法まで、すべて公開します。
第4層:解決の思想 ― keyプロップは「効率化」ではなく「生存戦略」
keyの「本来の意味」
Reactにおけるkeyは、元々 リストを効率的にレンダリングするための「添え字」 でした。
// リストの効率化(本来の用途)
{items.map(item => (
<ListItem key={item.id} data={item} />
))}「どの要素が追加・削除・移動されたか」をReactに教えることで、不要な再レンダリングを避ける。これが教科書的な説明です。
keyの「SSRでの意味」 ― 身分証明書
しかし、SSRという歪んだ構造においては、keyの役割が変質します。
keyは「効率化」ではなく、システムに「お前は間違いなく本人だ」と認めさせるための「身分証明書」になります。
// Before: 身分証明書なし
// → Reactは毎回「お前は誰だ?」と疑い、殺して作り直す
{loaderData.flashMessage && (
<FlashMessage
message={loaderData.flashMessage}
autoDismiss={true}
autoDismissDelay={5000}
/>
)}
// After: 身分証明書あり
// → 「同じメッセージ = 同一人物」とReactが認識する
{loaderData.flashMessage && (
<FlashMessage
key={loaderData.flashMessage} // ← 身分証明書
message={loaderData.flashMessage}
autoDismiss={true}
autoDismissDelay={5000}
/>
)}古い道具を新しい意味で使う
これは 建築家的なアプローチ です。
不自然な構造(SSR/ハイドレーション)を補強するために、古い道具(key)の定義を書き換える。道具の「本来の意味」に固執せず、「今の戦場で必要な意味」を与える。
解決策
ステップ1: keyプロップの追加
{loaderData.flashMessage && (
<FlashMessage
+ key={loaderData.flashMessage}
message={loaderData.flashMessage}
autoDismiss={true}
autoDismissDelay={5000}
/>
)}ステップ2: 依存配列の最小化
useEffect(() => {
if (!autoDismiss) return;
const timer = setTimeout(() => {
setIsVisible(false);
onClose?.();
}, autoDismissDelay);
return () => clearTimeout(timer);
- }, [autoDismiss, autoDismissDelay, onClose]);
+ }, [autoDismiss, autoDismissDelay]); // onCloseを除外ステップ3: 構造の理解をドキュメント化
CLAUDE.mdに以下を追記し、AIが同じ轍を踏まないようにします。
### SSR/ハイドレーション注意事項
- useEffectのタイマーが動かない場合、コンポーネントの再マウントを疑う
- 条件付きレンダリングのコンポーネントにはkeyを付与する
- keyは「効率化」ではなく「同一性の保証」として使う第5層:結論 ― AI時代の「延命医」の作法
3つの作法
AIは 「論理(Logic)」には強いが、「文脈(Context/歴史)」の衝突には疎い 。
「動かない」と嘆く前に、以下の3つを実践してください。
1. 道具の出自を疑え
Reactはどの時代の常識で作られたか? useEffectの「教科書的な動作」は、SPAを前提としている。SSRでは前提が異なる。
2. 構造の隙間を探せ
SSRとSPAの前提の違いはどこに表れるか? ハイドレーション、コンポーネントの同一性判定、ライフサイクルの二重実行。これらが「隙間」である。
3. 楔(くさび)を打て
keyという古い道具を、新しい意味で使え。「効率化のための添え字」ではなく「生存のための身分証明書」として。
魔法を信じるな、歴史を埋めろ
AIが生成するコードは「魔法」ではありません。それは過去の膨大なコードから抽出された「歴史の結晶」です。
その歴史が、あなたの現在の戦場と合致しているとは限らない。
歴史的な不自然さを技術者の眼力で見抜き、構造の隙間を埋めること。 これこそが、AI時代に自立を目指す者に必要な「防衛の知恵」です。
Remixでは同様のSSR環境固有の問題が他にも潜んでいます。CSSが突然消えた問題もパスエイリアスとNode.js/Workers環境の乖離が原因でした。また、import.meta.globが4つの複合原因でファイルを見つけられなかった問題も、「ローカルでは動くのに本番で動かない」という同じ構図から生まれています。
関連リソース
- React公式: リストとkey - keyの本来の用途
- Remix公式: ハイドレーション - SSRの制約
