Claude Code を React の作業に使い始めた頃、コードレビューなら自分が指摘していただろうコードを書いてくることに気づきました。

特に二つのアンチパターンが目立っていました。一つはすべてのイベントハンドラを useCallback で包む書き方、しかも依存配列が空のもの。もう一つは props から派生する値を useEffect の中で setState する書き方です。

大まかにはこんなコードでした。

const Panel = ({items, selectedId, onClose}: Props) => {
  const [item, setItem] = useState<Item | null>(null);
 
  useEffect(() => {
    const next = items.find((i) => i.id === selectedId);
    setItem(next ?? null);
  }, [items, selectedId]);
 
  const handleClickClose = useCallback(() => {
    onClose();
  }, []);
 
  // ...
};

useEffect は本来レンダリング中に済ませるべき計算を引き受けており、useCallback はメモ化する理由のないハンドラを包んだ上に、空の依存配列で初回レンダリングの onClose を凍結してしまっています。どちらもビルドは通る。誰もチェックしていなければ、そのまま本番に流れていきます。

動くコードと正しいコードはイコールではありません。パフォーマンスと技術的負債もコストですが、もっとやっかいなのが先例として残ること。本番に入ったアンチパターンはコードベースの一部になり、次のコンポーネントにコピーされ、「うちのやり方」になっていきます。アンチパターンが慣習に化けるわけです。

これは人間相手でも同じでしたが、AIで変わるのは強化の速度です。気づいた頃にはもう、それが支配的な書き方になっています。

二つのアンチパターン

ここで挙げたアンチパターンは、本番の React コードベースで何年も前から目にしてきたものです。シニアエンジニアがコードレビューで React のドキュメントへのリンクを添えて指摘する、あの定番の二つ。それを今、Claude が既定で生み出していました。

派生する値のための useEffect は、React のエコシステムでもっとも文書化されたアンチパターンの一つです。一致する itemitemsselectedId から導けるもので、同期すべき外部システムは存在しない。それなのに Effect を噛ませると、レンダリングサイクルが一つ増え、無限レンダリングへの入り口が開き、意図までぼやけてしまいます。

すべてのハンドラを useCallback で包むのも、よく見るパターンです。このフックが効くのは、memo でラップされた子コンポーネントに関数を渡すか、別のフックの依存として使う場合だけ。すべてを防御的に包めば、レンダリングのたびに依存配列を確保することになり、空の [] の裏に stale クロージャのバグを隠し込み、レビューする側にはそのラップが何のためにあるのかを毎回考えさせる羽目になります。上のスニペットでは、何ひとつ解決していません。

なぜClaudeはこう書くのか

React のチュートリアルではありません。チュートリアルはこの書き方を避けろと教えています。

AIの学習コーパスには、自分から望んで React を書いているわけではないエンジニア、あるいは現在の React の慣習に詳しくないエンジニアが書いた React のコードが、大量に含まれています。締め切りに追われたバックエンドエンジニアがフロントエンドのコードを書くこともあるし、「動くには動く」ままで本番へ流れていくコードもあります。

Claude が再現するのはコーパスの平均値。アンチパターンはその平均値の中にあります。

問うべきは「どうすれば賢いモデルが手に入るか」ではありません。Claude は良い React を書ける。問うべきは、平均値に流される前に、Claude の目にどう基準を届けるかです。

私がやったこと

React の公式 ドキュメントYou Might Not Need an Effect をもとに、React の作業のたびにプロジェクトが自動で読み込むスキルとしてまとめました。Claude が useEffect に手を伸ばそうとすると、まず次の六つの問いに答えさせられます。

  1. レンダリング中に計算で済ませられないか?
  2. ユーザー操作への応答ではないか?
  3. state から state への同期になっていないか?
  4. 親への state 変更通知になっていないか?
  5. props の変化に応じて子の state をリセットしたいだけではないか?
  6. 外部システムとの同期か?

最初の五つのどれかに「はい」なら、Effect は選ぶべき道具ではありません。同じスキルが、useCallback を使ってよい三つのケースも名指ししています。

  1. memo でラップされた子コンポーネントに渡すとき。
  2. 別のフックの依存として使うとき。
  3. 子コンポーネントがその関数をフックの依存配列で使うとき。

それ以外の場所では、関数は素のインライン関数のままです。

結果

アンチパターンは出てこなくなりました。

モデルが変わったわけではなく、基準がコンテキストに入っただけです。シニアエンジニアが手渡したであろう React チームの指針と同じものを、Claude も手にして、それに従ったということ。

このスキルから始まったもの

GAIAにはもともと、人間のエンジニアがよくある問題を踏まないように、静的チェックの仕組みが組み込まれていました。狙いは、PRをレビューするシニアエンジニアが本物のアンチパターンに集中できること。残りのコードはすでに正しい状態に揃っているという前提があったからです。

同じ思想をAIにも当てはめてみる、そう考えたのがGAIAをAIガードレールへと広げるきっかけでした。AIもまたエンジニアの一人で、チームのコンテキストを持たないままコードベースに入ってくるのは、新しく加わるどんなエンジニアでも同じ。だから同じループを回します。作業をレビューし、パターンを教え、基準を仕込む。

学び続けるという話は、熟練したエンジニアでも変わりません。違いはAIがセッションの終わりにコンテキストを失うことで、だからこそ基準はプロジェクト側に、コードとして、頼まなくても Claude が読み取れる場所に置かれていなければなりません。フック用スキルはその最初のピース。それ以来、TypeScript 用、Tailwind 用、テスト用のスキル、そしてハードコードされたJSX文字列を検出する Claude Code のフックを追加してきました。同じ原則を、毎回違うタイプのエンジニアに当てはめているだけです。

その原則は移植可能です。人間のエンジニアに対してシニアレビュアーが指摘するであろうものは、すべてコードに落として、Claude が自動で従えるようにできます。

GAIAは github.com/gaia-react/gaia でオープンソース公開しています。