1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

経験浅いエンジニアが AI に React 書かせてると、進化に取り残される説

1
Posted at

経験浅いエンジニアが AI に React 書かせてると、進化に取り残される説

📝 はじめに

こんにちは、シュートです。前回は「就活中のジュニアエンジニアが面接で痛感した『力不足』の話」を書きました。読んでくださった方、ありがとうございました。

その記事の最後で、「AIに頼って速さだけで進まず、一つひとつちゃんと理解してから次に進む」と書きました。今回はその続編、というか関連する別の気づきです。

アルバイトと個人開発で、React と TypeScript を書いています。エンジニア歴は1年ほどで、正直、React 自体が進化していることを「リアルタイムで体感する」ような経験はあまりありません。勉強し始めた頃と今、自分の中ではそんなに違いを感じていませんでした。

でも、最近ふと気づいたんです。

AI に React のコードを書かせると、結構な確率で「古い書き方」が返ってくる。

フォームの状態が手動の useState だらけだったり、ローディング管理が isLoading の手動切り替えだったり。それが自分にとっての"普通"になっていたんですが、よく調べると React 19 ではもっとシンプルに書ける方法が用意されていました。

経験の浅い自分は、何が古くて何が新しいか判断する基準を持っていないので、AI が書いたコードをそのまま「これが React の書き方」として吸収してしまっていた、ということです。

この記事では、その気づきを お問い合わせフォーム という具体例で確かめてみます。
ちなみに今回 AI に書かせたのは、現時点で最新の ChatGPT(GPT-5.5) です。「古いモデルだからでしょ?」というツッコミは入らないバージョンです。

同じように「AI に書かせて動いたから、それを React の書き方だと思ってる」ジュニアエンジニアに、何かしらの参考になれば嬉しいです。

💻 試してみた ── お問い合わせフォーム

AI に投げたプロンプト

使ったのは ChatGPT(モデル: GPT-5.5)です。

React と TypeScript で、シンプルな問い合わせフォーム(名前、メールアドレス、メッセージの3項目)を作ってください。送信時に API に POST する処理と、送信中のローディング表示も含めてください。

返ってきたコード

import { useState } from "react";

type ContactFormData = {
  name: string;
  email: string;
  message: string;
};

export default function ContactForm() {
  const [formData, setFormData] = useState<ContactFormData>({
    name: "",
    email: "",
    message: "",
  });
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [successMessage, setSuccessMessage] = useState("");
  const [errorMessage, setErrorMessage] = useState("");

  const handleChange = (
    e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
  ) => {
    const { name, value } = e.target;
    setFormData((prev) => ({ ...prev, [name]: value }));
  };

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setSuccessMessage("");
    setErrorMessage("");
    setIsSubmitting(true);
    try {
      const response = await fetch("/api/contact", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(formData),
      });
      if (!response.ok) throw new Error("送信に失敗しました");
      setSuccessMessage("お問い合わせを送信しました。");
      setFormData({ name: "", email: "", message: "" });
    } catch (error) {
      setErrorMessage("送信中にエラーが発生しました。もう一度お試しください。");
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">名前</label>
        <input
          id="name"
          name="name"
          type="text"
          value={formData.name}
          onChange={handleChange}
          required
          disabled={isSubmitting}
        />
      </div>
      <div>
        <label htmlFor="email">メールアドレス</label>
        <input
          id="email"
          name="email"
          type="email"
          value={formData.email}
          onChange={handleChange}
          required
          disabled={isSubmitting}
        />
      </div>
      <div>
        <label htmlFor="message">メッセージ</label>
        <textarea
          id="message"
          name="message"
          value={formData.message}
          onChange={handleChange}
          required
          disabled={isSubmitting}
          rows={5}
        />
      </div>
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "送信中..." : "送信する"}
      </button>
      {successMessage && <p style={{ color: "green" }}>{successMessage}</p>}
      {errorMessage && <p style={{ color: "red" }}>{errorMessage}</p>}
    </form>
  );
}

(スタイル定義はオリジナルにはありますが、本質ではないので省略しています)

念のため、これは 2026年時点で最新の GPT-5.5 が返してきたコードです。古いモデルではありません。

これも、自分の中では普通の書き方でした

正直に言うと、自分が書くフォームもだいたいこれと同じ構造です。

  • useState を4つ作る(formDataisSubmittingsuccessMessageerrorMessage)
  • onChange で都度入力値を state に保存する
  • handleSubmitsetIsSubmitting(true) → fetch → setIsSubmitting(false)

「フォームの状態は React の state で管理する」── 自分の中では絶対のルールでした。

でもこれ、React 19 では 半分以上が要らない ことを知ってしまったんです。

React 19 だとこう書ける

import { useActionState } from "react";

type FormState = {
  success: boolean;
  message: string;
};

async function submitContact(
  prevState: FormState,
  formData: FormData
): Promise<FormState> {
  const data = {
    name: formData.get("name") as string,
    email: formData.get("email") as string,
    message: formData.get("message") as string,
  };

  try {
    const response = await fetch("/api/contact", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(data),
    });
    if (!response.ok) throw new Error("送信に失敗しました");
    return { success: true, message: "お問い合わせを送信しました。" };
  } catch (error) {
    return {
      success: false,
      message: "送信中にエラーが発生しました。もう一度お試しください。",
    };
  }
}

export default function ContactForm() {
  const [state, formAction, isPending] = useActionState(submitContact, {
    success: false,
    message: "",
  });

  return (
    <form action={formAction}>
      <div>
        <label htmlFor="name">名前</label>
        <input id="name" name="name" type="text" required disabled={isPending} />
      </div>
      <div>
        <label htmlFor="email">メールアドレス</label>
        <input id="email" name="email" type="email" required disabled={isPending} />
      </div>
      <div>
        <label htmlFor="message">メッセージ</label>
        <textarea id="message" name="message" required disabled={isPending} rows={5} />
      </div>
      <button type="submit" disabled={isPending}>
        {isPending ? "送信中..." : "送信する"}
      </button>
      {state.message && (
        <p style={{ color: state.success ? "green" : "red" }}>{state.message}</p>
      )}
    </form>
  );
}

何が起きているのか

ポイントは2つです。

<form action={関数}> で送信処理を渡す

onSubmit の代わりに action 属性に関数を渡せるようになりました。
e.preventDefault() も不要、引数として FormData が自動で渡ってきます。input の name 属性さえあれば、formData.get("name") で値を取れます。

useActionState が状態を一括で面倒みてくれる

const [state, formAction, isPending] = useActionState(submitContact, 初期値);

これ1行で、

  • state: アクションの結果(成功メッセージ・エラーメッセージなど)
  • formAction: form の action に渡す関数
  • isPending: 送信中かどうか(自動で true / false が切り替わる)

の3つがまとめて返ってきます。useState を4つ作る必要が、まるごと無くなる ということです。

Before / After 比較

項目 Before(React 18 以前) After(React 19)
useState の数 4個 0個
handleChange 必要(各入力ごとに state 更新) 不要(uncontrolled でOK)
e.preventDefault() 必要 不要
ローディング状態 手動で setIsSubmitting(true/false) 自動(isPending)
input の value / onChange 全 input に書く 不要(name 属性だけでOK)

行数で言うと体感3割減、それ以上に 「フォーム処理の本質」だけが残った 感覚があります。submitContact 関数を見れば、このフォームが何をしているか一目瞭然。

…これ、知らないと一生 useState を4個作り続ける気がします。
しかも今回試したのは GPT-5.5、つまり 最新の AI でもこの書き方を返してくる わけで、自分から「React 19 で書いて」と指示しない限り、ずっと古いままのコードを書き続けることになります。

🔍 なぜ AI は古い書き方を返すのか ── 調べてみた

最新の GPT-5.5 が、なぜ React 17 時代の書き方を返してくるのか。
ただ「AIあるある」で終わらせるのも気持ち悪かったので、少し調べてみました。

1. 学習データのカットオフ自体は問題ないはず

OpenAI のドキュメントによると、GPT-5.5 の学習データのカットオフは 2025年12月1日(Dec 01, 2025)です。

一方で、React 19 の正式リリースは 2024年12月5日

つまり、GPT-5.5 のカットオフ時点では、React 19 が出てから ちょうど1年経過しています。理論上は、useActionState<form action={...}> の存在を「知っている」はず なんですよね。

…なのに、知らない人みたいなコードを返してくる。なぜなのか。

2. 学習データの中で「古い書き方」が圧倒的多数派

これが本質的に一番大きな要因だと思います。

ネット上のコード資産 ── GitHub のリポジトリ、StackOverflow の回答、技術ブログ、Qiita 記事 ── これらの大半は、React 19 が出る前に書かれたものです。当然、フォームは useState + onSubmit で書かれています。

LLM は、学習データの中で 「よく出てくるパターン」を高頻度で再現する 性質があります。プロンプトに「最新の」「React 19 で」のような指示がなければ、自然と多数派の書き方が出力されます。

  • 古い書き方のサンプル数: 膨大(React 16, 17, 18 時代の蓄積)
  • 新しい書き方のサンプル数: 少ない(React 19 リリースから1〜2年分)

確率的に古い書き方が出てくるのは、ある意味当然です。

3. 公称カットオフと「実際に学習に効いたデータ」にギャップがある

もう一つの可能性。

「カットオフ日 = 2025年12月1日」と表示されていても、それは「この日付までのデータが含まれている」というだけで、直前のデータが学習に十分寄与しているとは限らない という指摘が、最近の研究や開発者コミュニティで出ています。

カットオフ日付近のデータは、量も質も薄くなる傾向がある。React 19 リリース直後のブログ・コード・解説記事も、まだ十分な量が出揃う前にカットオフされた可能性があります。

4. これは構造的な問題で、研究でも実証されている

2026年4月に発表された arXiv 論文 "When LLMs Lag Behind: Knowledge Conflicts from Evolving APIs in Code Generation" (Ashik et al., 2026) では、まさにこの問題 ── LLM がライブラリの進化に追いつけず、deprecated な API を生成し続ける問題 ── が定量的に研究されています。

タイトルがすべて言い尽くしている感じですが、要するに 「LLM はライブラリの進化から構造的に遅れる」 という現象は、自分が個人的に感じた違和感ではなく、学術的に認められた現象なんです。

つまり、こういうことらしい

整理すると、AI が古い書き方を返してくる理由は以下のとおりです。

要因 内容
① 学習データの量的偏り ネット上に React 18 以前のコードの方が圧倒的に多い
② カットオフと実態のギャップ カットオフ日付近のデータは収録が薄い傾向
③ 多数派バイアス LLM は確率的に「よく見るパターン」を優先して出力する
④ ライブラリ進化への追従遅れ 学術的にも構造的な問題として実証されている

つまり、これは GPT-5.5 がたまたまサボってるとか、自分の運が悪かったとかではない
LLM という仕組みに内在する 構造的な問題 です。

新しいフレームワーク、新しい書き方ほど、AI は「知ってはいるけど、自然には出してこない」状態になりやすい。React 19 はその典型例にハマってしまっている、ということになります。

💡 これからどう向き合うか

「AI の仕組み的に古いコードが出てくるのは仕方ない」と分かった上で、では自分はどうしていくか。
ジュニアエンジニアなりに、これからやっていこうと思っていることをいくつか書きます。

1. プロンプトに「最新の React で」を明示する

一番手っ取り早い対策です。
ただ「React でフォームを書いて」だと古い書き方が返ってきますが、最初から「React 19 の useActionStateform action を使って書いて」と指示すれば、ちゃんと新しい書き方を返してきます。

つまり、AI に任せるなら、「こちらが最新を知っている」ことが前提 になります。

(余談ですが、これに気づくと「AI に丸投げ」がそもそも無理ゲーだったことが分かります。何が最新か分かる人だけが、AI を正しく使える。)

2. AI が書いたコードに「疑いの目」を持つ

これは前回記事「就活中のジュニアエンジニアが面接で痛感した『力不足』の話」の続きでもありますが、AIが書いたコードを「動いたからOK」で済ませない。

特に怪しいパターンは、

  • useState が4個も5個も並んでいる
  • forwardRefuseImperativeHandle のような複雑な仕組みを使っている
  • データ取得が useEffect の中で書かれている

これらを見たら一回、「これって2026年の React 的に、まだベストな書き方なのか?」 と立ち止まる。完全に分かってなくても、「疑う」だけならジュニアでもできる ことです。

3. 「進化していること」を前提に扱う

これが一番大事かもしれません。

経験が浅い自分は、勉強し始めた頃に「React の書き方はこれが正解」と固定的に学んでいました。でも、フレームワークもライブラリも、毎年のように進化しています。
「今の自分の書き方は、いつか必ず古くなる」 ── この前提を持っておけば、定期的にキャッチアップする習慣を作るしかなくなります。

AI は便利ですが、AI 自身は進化に追いつくのが構造的に遅い。
だから 人間側が、ライブラリの進化に対する解像度を持っておく 必要がある、ということだと思います。

🙌 おわりに

今回の気づきをまとめると、こんな感じです。

  • AI に React を書かせると、最新モデルでも古い書き方が返ってくる
  • それは GPT-5.5 がサボってるからではなく、LLM の構造的な問題
  • だから AI に丸投げできず、自分が「最新を知っている」状態を作るしかない

正直、最初は「AI を使えば、自分が知らないことでも書けるじゃん」と思っていました。でも調べていくうちに、それは半分くらい幻想だったことが分かりました。

AI が代わりに勉強してくれるわけではないし、むしろ AI が古い書き方を再生産する仕組みである以上、人間の側が新しさを学び続けないと、書くコードはどんどん古くなっていく ということなんですよね。

経験が浅いジュニアエンジニアこそ、ここを意識しないと取り残されるかもしれません。
同じように「AI に書かせて動いたから、それが React の書き方」と思っていた方、一緒に地道にキャッチアップしていきましょう💪

最後まで読んでいただき、ありがとうございました!


参考リンク

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?