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

著者: ClaudeMix Team |
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つの複合原因でファイルを見つけられなかった問題も、「ローカルでは動くのに本番で動かない」という同じ構図から生まれています。

関連リソース