React + Rails APIでのリプレイス開発を通して、責務設計の変化について
はじめに
本記事は、「React + Rails API構成で作る」(認証状態の責務、Server Stateの責務)を考え、
- zustandを採用した話
-
TanStack Queryを採用した話
の続きとして、ポートフォリオアプリを作成した際に得た知見をもとにアウトプットとして執筆したものです。
注意
本記事は、実務未経験のポートフォリオ開発を経て、得た知見を基に作成しています。
拙ない表現や、認識の甘さが見つかった際はご教授いただけると幸いです。🙇♀️
詳しくは、記事リンク、および、GitHubリンク、アプリリンク先にて確認していただくと幸いです。
GitHubページ :https://github.com/yuji-2293/ColorMirror_Re
アプリへのリンク: https://color-mirror-re.vercel.app/
旧アプリの課題
ポートフォリオとして開発した「ColorMirror」では、Rails + Turbo構成で開発を行いました。
GitHubページ: https://github.com/yuji-2293/ColorMirror
アプリへのリンク: https://colormirror.jp/
※現在公開停止中
このアプリをポートフォリオとして開発、振り返った時考えた課題について整理していきたいと思います。
アプリが提供するUXを実現するためには複数の機能を1画面に統合する必要があった
アプリのコアな機能は、ユーザーが気分を入力することで各機能を順番に体験していく構成をとっています。
コア機能の構成図
これらの機能を順番に実行しながら、1画面の中でUXを提供する構成をとっていました。
そのため、ユーザーの入力に沿って、複数の機能を繋ぎ合わせることでアプリの提供する体験を実現させていました。
しかし、いざ開発を終えてみると、複数の機能を制御するためにコードの管理が複雑化してしまいました。
結果として、1つのファイルに責務を混ぜてしまい、可読性が下がった保守性に乏しいコードで構成したアプリとなってしまいました。
実際のコード
document.addEventListener("turbo:load", () => {
const moodButtons = document.querySelectorAll(".mood-button");
const selectedMoodInput = document.getElementById("selected-mood");
const selectedWeatherInput = document.getElementById("description");
const generateColorsButton = document.getElementById("generate-colors");
const colorSuggest = document.getElementById("color-suggest");
// イベント制御
moodButtons.forEach(button => {
button.addEventListener("click", () => {
selectedMoodInput.value = button.dataset.mood;
});
});
generateColorsButton.addEventListener("click", () => {
const mood = selectedMoodInput.value;
const weather = selectedWeatherInput.value;
// fetch通信
fetch("/colors/analyze", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ mood, weather })
})
.then(response => response.json())
.then(data => {
colorSuggest.innerHTML = "";
// UI生成
data.colors.forEach(color => {
const colorBall = document.createElement("div");
colorBall.style.backgroundColor = color.hex;
colorBall.dataset.color = color.hex;
const colorLabel = document.createElement("p");
colorLabel.textContent = color.name;
const colorContainer = document.createElement("div");
colorContainer.appendChild(colorBall);
colorContainer.appendChild(colorLabel);
colorSuggest.appendChild(colorContainer);
});
setupColorBallClickEvents();
});
});
function setupColorBallClickEvents() {
const submitColor = document.querySelectorAll(".color-ball");
submitColor.forEach(submit => {
submit.addEventListener("click", (event) => {
const colorName = window.getComputedStyle(event.target).backgroundColor;
document.getElementById("color_name").value = colorName;
});
});
}
});
このコードでは1つのファイルに以下の責務が混在しています。
- 気分を選択時のイベント制御
- Rails Controllerへfetch
- 取得したデータを元にしたDOM生成
- ユーザーが選択した要素の状態保持
- 選択したcolorをhidden fieldにセット
実装時は「動く」ことや機能として成立することを優先して実装しましたが、機能追加していくにつれて責務の境界が見えづらくなっていました。
特に、取得したデータを元にDOMを生成した後、そのDOMに対して再度イベントを付与する必要があり、機能が増えるほどJavaScript側の責務が膨らみやすい構成になっていました。
React + Rails API構成へリプレイスして責務はどう変わったのか
- 機能単位で優先した実装により責務が曖昧になってしまう
- 機能が増えるにつれ、1つのファイルの持つ責務が膨らんでしまう
これらの課題を解決することを目的として、「ColorMirror_Re」をリプレイス開発することにしました。
旧アプリとの責務の比較
| 旧ColorMirror | ColorMirror_Re |
|---|---|
| JavaScriptでfetch通信 | axiosが通信責務を担う |
| JavaScriptでDOMを操作 | ComponentがUI構築の表示責務を担う |
| JavaScriptが状態を保つ | useState/ zustand / TanStack Query が状態管理の責務を担う |
責務設計の比較図
責務設計の比較
├── 旧アプリ(ColorMirror)/
│ └── app/
│ └── JS/
│ ├── fetch
│ ├── DOM操作
│ ├── 状態保持
│ └── イベント処理
リプレイスアプリ(ColorMirror_Re)/
├── back/
│ └── Rails API
└── front/
└── React/
├── 通信/
│ └── axios
├── 画面遷移/
│ └── React Router
├── 状態管理/
│ ├── useState
│ ├── zustand
│ └── TanStack Query
└── UI構築/
└── Component
Reactの責務設計で元のアプリからどう変わるのか?
(例) 「ColorMirror」 app/js/color_fetch.js
旧アプリ(ColorMirror)
color_fetch.js
├── イベント制御
├── fetch
├── DOM生成
├── ユーザーが選択したUIの状態保持
└── hidden field制御
↓
1つのファイルに複数の責務が混在している状態
「ColorMirror_Re」 React構成にしたとき
↓
React + vite + TypeScript
features/
└── colors/
├── api/
│ └── colorsGetData.ts(API通信)
├── hooks
│ └── useColors.ts(状態管理)
└── Component
└── ui
└── colorsForm.tsx(UI構築)
責務ごとにファイルを分離することで、
通信・状態管理・UI構築がそれぞれに責務を持つことができる。
結果、機能単位で拡張・修正が行われ、特定しやすい構成にすることができる。
リプレイスの構成
React + Rails APIの構成をとることで、
フロント側は、
- API通信
- 状態管理
- UI構築
- 画面遷移
それぞれをライブラリやコンポーネントが担当することで、責務を混じらせずに閉じ込めることができました。
結果、旧ColorMirrorで「1つのファイルが複数の責務を持つ」状態から脱することで、機能追加や修正時の保守性の向上に繋がりました。
React + Rails APIの構成をとることで、
RailsはAPIサーバーとしてデータを返却、フロント側をReactでUIの構築に専念させることができました。
開発してみてわかったこと
今回のリプレイス開発を通して、旧アプリの課題だった「責務の曖昧さの解決」を目的にしてReactにおける技術構成を学ぶことができました。
旧アプリでは、通信・状態管理・UI構築といった複数の責務が1つのファイルに集約されており、責務の境界が曖昧になっていました。
リプレイス開発では、「責務単位」で再構成することで、API通信、状態管理・UI構築、それぞれの担う責務を閉じ込めることで、保守性の向上、機能追加にも耐えられる形で実装を行うことができました。
今回の開発を通して、旧アプリとの責務設計の違いを認識しました。
そこで、ライブラリは便利だから導入するのでなく、アプリが抱える課題や責務に応じて選定することの重要性を学ぶことができました。
まとめ
本記事では、Rails + Turboで開発した旧アプリを振り返り、
React + Rails API構成へリプレイスした際の責務設計の変化について整理しました。
- 旧アプリでは通信・状態管理・UI構築の責務が1つのファイルに混ざってしまった
- React + Rails API構成では責務ごとに分離して実装した
- ライブラリは便利だから導入するのではなく、課題や責務に応じて選定することが重要だと学ぶことができた
リプレイス開発を通して、技術を学ぶだけでなく、責務設計の重要性についても理解を深めることができました。
終わりに
ここまで読んでいただきありがとうございます。
下記の記事と本記事を通して、リプレイス開発で得た知見を整理して、技術記事として執筆させていただきました。
ライブラリの使用例としてでなく、アプリ開発の中でライブラリをなぜ採用したのかについて言語化する目的で始めた3本立ての技術記事でした。
今後も、さまざまなキャッチアップを通して、記事として言語化し公開していくつもりです、何卒ご意見ございましたらよろしくお願いいたします。