【Cursor活用】レガシーなReactコンポーネントを2時間でモダン化した実践テク

CursorでReactのクラスコンポーネントをhooksにリファクタリング

リード文

レガシーなReactのクラスコンポーネントを関数コンポーネント + hooksに移行する作業、手作業なら1日がかりですが、Cursorを活用したら約2時間で5つのコンポーネントの移行が完了しました。この記事では、実際に使ったプロンプト設計のポイントと、リファクタリングを効率化するための具体的なテクニックを解説します。

背景・課題

プロジェクトには古いReactのクラスコンポーネントが多数残っており、メンテナンス性が低下していました。

従来の手作業でのリファクタリングには以下の問題がありました:

  • ライフサイクルメソッドの理解に時間がかかる:componentDidMount、componentDidUpdate、componentWillUnmount の複雑な依存関係を読み解く必要がある
  • 状態管理の変換ミスが起きやすい:this.stateからuseStateへの移行で、setStateの非同期処理やコールバックの扱いを間違えやすい
  • TypeScriptの型定義の整合性確認が面倒:Props、State、イベントハンドラーの型を一つ一つ確認する必要がある
  • 関連ファイルへの影響範囲の把握が困難:コンポーネントを利用している親コンポーネントや子コンポーネントへの影響を手動で追跡するのは大変

これらの理由から、1コンポーネントあたり2〜3時間かかり、5つ移行するなら丸1日以上の作業時間が見込まれていました。

使用したツール・環境

  • ツール名:Cursor
  • バージョン:最新版(2024年12月時点)
  • 対象コードベース:React 18 + TypeScript
  • 前提条件
    • プロジェクトがTypeScriptで書かれていること
    • 既存のテストコードがあると検証が楽(必須ではない)
    • Cursorがプロジェクト全体のコンテキストを理解できる状態にしておく

実際に使ったプロンプト

使用したプロンプト

このクラスコンポーネントを関数コンポーネント + hooksに書き換えてください。

要件:
- useState, useEffectを適切に使用
- 既存の機能は維持
- TypeScriptの型を維持・改善
- 不要なライフサイクルメソッドは削除

プロンプト設計のポイント

このプロンプトでは、以下の点を意識して設計しました:

1. 「既存の機能は維持」を明示して勝手な変更を防止

AIは時々「こうした方がいい」と判断して余計な機能追加や変更を行うことがあります。「既存の機能は維持」と明示することで、リファクタリングの範囲を「書き方の変更のみ」に限定できます。

2. TypeScriptの型維持・改善を指定

クラスコンポーネントからhooksへの移行時、型定義が曖昧になりがちです。「TypeScriptの型を維持・改善」と指定することで、any型への逃げや型の緩和を防ぎ、むしろ型安全性が向上するケースもありました。

3. useState、useEffectを明示的に指定

「hooksに書き換えて」だけだと、どのhooksを使うべきか曖昧になります。useState、useEffectを明示することで、状態管理とライフサイクル処理の変換方針が明確になり、出力の精度が上がります。

4. 「不要なライフサイクルメソッドは削除」で無駄なコードを残さない

クラスコンポーネントには、実際には使われていないライフサイクルメソッドが残っていることがあります。この指示により、本当に必要なロジックだけを抽出してくれます。

5. 失敗したパターンと改善

最初は「書き換えて」とだけ指示したところ、以下の問題が発生しました:

  • useEffectの依存配列が不適切で、無限ループが発生
  • 元のコードにあった細かいエッジケース処理が抜け落ちる
  • 型定義がany型に緩和されてしまう

「既存の機能は維持」「型を維持・改善」という制約を追加することで、これらの問題がほぼ解消されました。

実行結果・効果

5つのクラスコンポーネントを約2時間で移行完了(手作業なら1日かかる見込みでした)

具体的な成果:

  • 作業時間の削減:1コンポーネントあたり平均24分(手作業なら2〜3時間)
  • コード行数の削減:平均30%減(クラス特有のボイラープレートが不要に)
  • 型安全性の向上:Cursorが提案した型定義により、3箇所で潜在的なバグを事前に検出
  • 関連ファイルへの影響把握:Cursorがimport文の修正や、親コンポーネントでの型変更も提案してくれた

実際の出力例(一部抜粋):

// Before: クラスコンポーネント
class UserProfile extends React.Component<Props, State> {
  componentDidMount() {
    this.fetchUser();
  }
  // ...
}

// After: 関数コンポーネント + hooks
const UserProfile: React.FC<Props> = ({ userId }) => {
  const [user, setUser] = useState<User | null>(null);
  
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);
  // ...
};

想定外だった良い点:

Cursorがコードベース全体を見てくれるため、「このコンポーネントを使っている親コンポーネントでも型定義を更新した方がいいですよ」といった関連ファイルの修正提案までしてくれました。これにより、リファクタリング後の整合性チェックの手間が大幅に削減されました。

応用パターン・カスタマイズ例

1. Redux連携のクラスコンポーネントを移行する場合

connect()を使ったRedux連携コンポーネントの場合、プロンプトを以下のように変更:

このRedux連携クラスコンポーネントを関数コンポーネント + hooksに書き換えてください。

要件:
- connect()をuseSelector, useDispatchに置き換え
- mapStateToProps、mapDispatchToPropsのロジックは維持
- TypeScriptの型を維持・改善
- 既存の機能は維持

2. 複雑なライフサイクル依存がある場合

componentDidUpdateで複雑な条件分岐がある場合:

このクラスコンポーネントを関数コンポーネント + hooksに書き換えてください。

要件:
- componentDidUpdateの条件分岐を複数のuseEffectに適切に分離
- 依存配列を明示的に記載し、コメントで理由を説明
- 既存の機能は維持
- TypeScriptの型を維持・改善

3. パフォーマンス最適化を含める場合

// useMemo、useCallbackを活用した最適化例
const UserList: React.FC<Props> = ({ users, onUserClick }) => {
  const sortedUsers = useMemo(() => 
    [...users].sort((a, b) => a.name.localeCompare(b.name)),
    [users]
  );
  
  const handleClick = useCallback((userId: string) => {
    onUserClick(userId);
  }, [onUserClick]);
  
  // ...
};

注意点・限界

1. 複雑なライフサイクルの依存関係は手動確認が必須

componentDidUpdate内で複数のpropsやstateを比較している場合、useEffectの依存配列が不適切になることがあります。Cursorの出力をそのまま信用せず、必ずロジックを読んで確認してください。

2. テストは必須

リファクタリング後は、必ずユニットテストや統合テストを実行してください。特に以下のケースで問題が起きやすい:

  • 非同期処理のタイミング
  • イベントハンドラーのthisバインディング
  • 子コンポーネントへのprops受け渡し

3. パフォーマンスの変化に注意

クラスコンポーネントからhooksへの移行で、再レンダリングの挙動が変わることがあります。特にuseEffectの依存配列が不適切だと、無限ループや過剰な再レンダリングが発生する可能性があります。

4. レガシーなパターンの扱い

componentWillReceivePropsなど、React 18では非推奨のライフサイクルメソッドを使っている場合、Cursorも完全には対応できないことがあります。その場合は、まず公式のマイグレーションガイドを参照してください。

まとめ

  • Cursorを使えば、クラスコンポーネントからhooksへの移行が大幅に効率化される(5コンポーネント2時間 vs 手作業1日)
  • プロンプトで「既存の機能は維持」「型を維持・改善」を明示することで、勝手な変更や型の緩和を防げる
  • 複雑なライフサイクル依存やテストは人間の確認が必須。AIの出力を盲信しない

まず試してみてほしいこと:
小規模なクラスコンポーネント(state 1〜2個、ライフサイクル1〜2個程度)から試して、Cursorの出力パターンに慣れることをおすすめします。慣れてきたら、より複雑なコンポーネントにも適用できるようになります。