♻️

Rails + jQuery構成からReactへ──現場で進めた共存戦略と段階的移行

に公開

はじめに

こんにちは。スマサテで開発を担当している佐治です。スマサテでは昨年から段階的にプロダクトの画面をReactへ移行を進めており、今回はここまで得られた段階的な移行の知見・ノウハウを紹介します。

React化の背景

スマサテのフロントエンドはRails + Slim + jQuery構成で、表示ロジックが複数ファイルに分散していました。一つの機能修正でSlimテンプレート、CSS、jQueryファイルを横断的に確認する必要があり、開発効率が低下していました。

React化により関連コードを一つのコンポーネントに集約したいところですが、新機能開発も並行して進める必要があり、一度に移行することは現実的ではありませんでした。そこで、jQueryとReactを共存させながら徐々に置き換えていく戦略を採用しました。


査定条件入力画面の置き換え例

ここからは最近行った査定条件入力画面のReact化を例に、具体的な実装戦略を紹介します。

査定条件入力画面は多数の入力項目を持ち、項目間の依存関係も複雑でした。画面内の全ての入力項目を一度にReact化するのではなく、一部の入力項目のみを選択的にReact化し、既存のform_with構造やjQuery実装と共存させる方針を採用しました。

実装時の配慮点:

  • Rails側の送信処理は変更しない
  • React化した項目と、jQuery実装のまま残した項目の連携を保つ
  • ユーザ設定による動的な表示切り替えに対応

以下、採用した2つの戦略を紹介します。


戦略①:「完全リプレースしない」段階的移行設計

フォーム全体を一気にReactへ置き換えるのではなく、
Slimのform_with構造は残しつつ内部の一部フィールドをReact化する という設計を採用しました。

Reactで描画した入力要素は既存のform_withの管理外となるため、そのままではRailsにデータが送信されません。そこで、form_with内にUI上では見えない要素であるhidden fieldを配置し、React側の値変更をリアルタイムで同期させることで、React化したフィールドも未対応のフィールドも同じ送信処理で扱えるようにしました。

実装方針

  • 既存のRailsの form_with による送信処理は変更しない
  • 各フォーム要素を個別にReact化し、その値をform_with のhidden fieldに反映
  • バリデーションはReactとjQuery両方で実施し、どちらもOKなら送信

Hidden Field管理

React側の状態とRails側のフォームを繋ぐために、専用の管理クラスfieldManagerを用意しました。このクラスは以下の役割を担います:

  • 入力値の同期:テキストフィールドやセレクトボックスの値をhidden fieldに反映
  • DOM操作の一元化:Reactコンポーネントから直接DOM操作を行わず、責務を分離
export class FieldManager {

  // ...

  updateField(fieldPath: string, value: string): void {
    const element = document.getElementById(fieldPath);
    if (element) {
      (element as HTMLInputElement).value = value;
    }
  }
}

React側での活用

react-hook-formと組み合わせて、フォーム値の変更を監視し、自動的にhidden fieldを更新する仕組みを構築:

useEffect(() => {
  const subscription = methods.watch((value, { name }) => {
    // フォーム値が変更されたらhidden fieldを更新
    fieldManager.updateField(name, value);
  });
  return () => subscription.unsubscribe();
}, [methods, fieldManager]);

なぜこの形にしたのか

  • 既存の送信処理を完全に維持 - Rails側のパラメータ構造を変更せず、既存のコントローラーやモデルへの影響を回避
  • 段階的な移行が可能 - 一つずつフィールドをReact化でき、問題があれば即座に元に戻せる
  • 複雑な連携機能も維持 - 保存済み条件のコピー機能など、既存のjQuery実装との連携も可能

この設計により、ビジネスロジックに影響を与えることなく、UIレイヤーのみを段階的にモダナイズすることができました。


戦略②:既存DOMイベントとの共存 ― addEventListenerフック

査定条件入力画面では、入力項目間に依存関係があります。例えば、ある項目の選択内容によって別の項目がdisabledになったり、表示/非表示が切り替わったりします。React化していない既存のjQuery実装と連携するための方法を統一しました。

実装方針

  • Reactコンポーネントがマウントされたあとに、既存のDOMイベントを監視
  • jQuery側からのイベントを受け取り、React内部状態を更新
  • React側で状態変更が起きた際も、必要に応じて外部へイベントを発火
useEffect(() => {
  const handlePatternChange = (e: CustomEvent) => {
    const pattern = e.detail.pattern;
    // jQuery側で受け取ったイベントとパラメータに応じてReact側の内部状態を更新
  };
  window.addEventListener("patternChanged", handlePatternChange);
  return () => window.removeEventListener("patternChanged", handlePatternChange);
}, []);

こうすることで、React側で管理しているフォームコンポーネントに対して、
既存jQueryコードからも安全に値を反映させることができます。

メリット

  • 段階的なReact移行が可能
  • 既存の仕組み(コピー処理・他フォームとの連携)を一時的に維持できる
  • チーム全体で移行の優先度を調整しやすい

実装時の学び

上記のように段階的移行を進めたのですが、実際に進めると予想以上に時間がかかりました。

徹底したコードリーディングの必要性

React化を進める上で最も重要だったのは、既存コードの完全な理解でした。

HTMLのclass名やid属性一つ変更する場合でも、まず関連するファイル全体を検索して依存箇所を洗い出し、各依存がどのタイミング・条件で動作するかを把握する必要がありました。さらに、エッジケースや例外処理も含めて影響範囲を明確にし、実際の動作確認を通じて既存の仕様と挙動を把握した上でReact化の戦略を立てていきました。

この地道な作業なしに進めると、思わぬ不具合に直面することになります。

レガシーコードのリプレースは、新機能開発とは異なる技術力が求められます。 それは、既存の複雑な仕様を正確に読み解き、完全に再現する力です。この経験を通じて、コードリーディングの重要性を改めて認識しました。

改めてReact化に付き合っていただいた開発メンバーの皆さんに感謝申し上げます & これからもよろしくお願いします。


おわりに

今回は、jQuery + Rails構成からReactへの段階的移行を紹介しました。

「完璧なReact化」を目指すのではなく、既存の仕組みと共存しながら「止まらないReact化」を進めることで、ビジネスへの影響を最小限に抑えながら着実に改善を進めることができました。

レガシーシステムの移行は、技術的な挑戦だけでなく、既存の複雑な仕様と向き合う忍耐力も必要とされます。しかし、その過程で得られる深いシステム理解は、今後の開発にも活きる貴重な経験となりました。

スマサテではエンジニアを募集しています。こういったレガシーコードの改善以外にも様々な技術課題、新機能の開発をチームで取り組んでいます。ぜひWantedlyのページをご覧ください。
https://www.wantedly.com/projects/2099580

スマサテ Tech Blog

Discussion