プログラミング初心者がReact × Railsでアプリ作ってみた記録
プログラミング学習中のサラリーマンです。
今回は1週間くらい時間を使い、
ReactとRuby on Railsを使ってSPAを作ってみたので
自分の理解を深めがてら、機能の解説をしてみます。
決して難しいことはしていませんが、こんな簡単な機能を作るために結構いろんなことを書かないといけない、ということが伝わればいいな〜と。
コンポーネント解説のところには、それぞれGitHubのリンクを貼っておきます。
フレームワーク
サーバーサイド:Ruby on Rails 7.0.8.1
フロントエンド:React 18.3.1
環境はMacOS 14.4.1です。
あとChakra UIを使用しています。
作ったもの, 機能
タスク管理アプリを作りました。
タスク一覧表示機能
未完了のタスク一覧を表示できる
完了済みのタスクを表示/非表示できる
期日で昇順/降順ソートできる
タスク作成機能
タスク名が入力できる
タスクを入力した日を表示できる
期日が設定できる
タスク編集機能
タスクを個別に編集できる
編集時に詳細メモを追加できる
タスク完了機能
チェックボックスにチェックを入れると「完了」ステータスに変更される
完了ステータスになると
取り消し線がつく
タスク削除機能
削除したタスクはDBからも削除される
いわゆるCRUD(Create, Read, Update, Delete)を一通り実装しています。
API接続
Rails App構築
Rails AppはAPIモードで作成しました。
ターミナル内、アプリを作成したいディレクトリで以下を入力。
rails new todo_api --api
これでAPIモードでRails Appの雛形が構築できます。
いわゆるヘッドレスってやつですね。
ヘッドレスってのは、簡単に言えば「プレステをテレビに繋いでない状態」です。ゲームは起動しているし、操作もできるけど画面は見れないということです。
通常、RailsアプリではMVCの「V」の部分がフロントエンドを担当しますが、今回はReactでフロントを作り、MとCの部分だけをRailsが担当するようにします。
React App構築
ReactのAppは普通に作ります。ターミナルで以下を入力。
npx create-react-app todo-web
これでReactアプリの雛形ができます。
同じディレクトリにフロントAppとサーバーサイドAppを入れる
個人的には「アプリがいっぱい入っているディレクトリ」にRailsとReactのAppを入れてしまうのではなく、この2つのプロジェクトを保存するフォルダを作成して、専用のディレクトリにしてしまうほうが取り回しが良かったです。エディタでそのフォルダを開いておけば、横断的に編集できますからね。
起動サーバーの場所を設定
通常、localで起動するAppは、デフォルトでは放っておくとlocalhost:3000で起動します。
ですが、今回はAPIとしてのRailsアプリと、フロントを描画するためのReact アプリを両方起動したいので、どちらも3000を使うわけにはいきません。
(プレステとテレビを1つのコンセントに繋がないのと同じ感じですかね)
Rails側の設定:
localhost:3000からズラすのは片方でOKなので、Rails側を変更するようにします。
config/puma.rb
port ENV.fetch("PORT") { 3000 } // 削除
port ENV.fetch("PORT") { 3010 } // 追加
ポートを3010番にズラしておきます。これで1台のPCで同時に2つのアプリを起動し、相互に連携させることができます。
(参考にした資料では3010でしたが、AI先生に聞いた時は3001を例示されることが多かったので、きっと3001でも良い気もします)
ディレクトリ構造(フロントエンド)
React側のディレクトリ構造はこんな感じです。
(概略。勝手に生成されたけど触っていないものは書いていません)
コンポーネント志向の勉強のために、あえて細かめにコンポーネントを分けたつもりです。
src
├── App.js
├── api
│ └── taskApi.js
├── component
│ ├── Task.jsx
│ ├── TaskDetailModal.jsx
│ ├── TaskEditModal.jsx
│ ├── TaskForm.jsx
│ ├── TaskList.jsx
│ └── TaskTable.jsx
└── utils
└── dateUtils.js
apiディレクトリ・・・api側との通信を担当するコンポーネントだけを格納しました
componentディレクトリ・・・フロントエンド側を実装する各機能を持たせた各コンポーネントを格納しました
utilsディレクトリ・・・利便性のために追加する、必須ではない機能を実装するコンポーネントを格納しました
各コンポーネントの役割
各コンポーネントの簡単な役割を紹介します。
ここで説明できることが自分の理解度だと思います。
App.js
コードはこちら
このアプリのメインページ。ここにいろいろなコンポーネントを配置していきます。
具体的にここでやっていることは以下です。
タスクが全て格納された配列を定義
タスクの各プロパティ(名前や期日、メモ)を編集する関数を定義
APIからタスクのデータを受け取る関数を定義
タスクを期日ごとに並び替える関数を定義
完了したタスクを表示/非表示にする関数を定義
タスクを削除する関数を定義
タスクを新規作成するフォームを呼び出す(TaskForm.jsx)
登録済みのタスクを表示するリストを呼び出す(TaskList.jsx)
すべてのコンポーネントがここに集まって、ユーザーに表示される画面をレンダリングします。
taskApi.js
コードはこちら
フロントのReactとサーバーサイドのRailsが通信する部分だけ切り出したコンポーネントです。
本来このような実装が一般的なのかはわかりませんが、なんとなく単一責任原則に則るとアリな分け方だと思いました。
get = 既存のデータの取得
post = 新規データ(タスク)の追加
put = 既存データの編集
delete = 既存データの削除
この4つの操作がフロント側で行われたときに、それぞれのコンポーネントがRails側にデータを渡してくれます。
ちなみに、put(編集)する場面は「タスク完了・未完了の切り替え」と「タスク内容の編集」の2種類があるので、それぞれ別のコンポーネントが担当することにしました。
TaskForm.jsx
メイン画面に鎮座し、新規タスクを入力させるフォームをレンダリングするコンポーネントです。
参考にした資料ではタスク名のみの実装でしたが、期日も設定できるフォームに変更しました。
入力できる項目は、タスク名と期日のみです。
実際に自分で触ってみて、「期日入れるの面倒くさいな〜」と思ったのでデフォルトでは「明日」がセットされるようにしてみました。
(明日やろうぜばかやろー)
ここで工夫した点は、エンターキーで確定できるようにする点です。
参考にしたカリキュラムでは「単なるinput要素」で実装されていたのですが、これだと「タスクを作成」ボタンをクリックしないと確定できない問題がありました。
そこで、App.jsで呼び出す時、親要素にformを使うことで「入力してreturnキーを押すとタスクが作成される」という挙動にしました。
TaskList.jsx
コードはこちら
このコンポーネントでは、タスクを期日でソートしたり、完了したアイテムを非表示にする/表示する といった配列管理を行なっています。
ついでに、それらの機能をもつボタンもレンダリングしています。
Reactでは、配列を変更したいとき「元の配列を加工するのではなく、別で用意した新しい配列で置き換える」という操作が基本になっています。
元の配列を加工してしまうのはミューテーション(変異)と呼ばれ、思わぬトラブルのもとになるので出来るだけ避けるように、と公式チュートリアルに書いてありました。
ソートした結果のタスクリストを用いて、TaskTableコンポーネントを呼び出しています。
TaskTable.jsx
コードはこちら
このコンポーネントは、TaskListから受け取ったソート/フィルタ済みのタスクリストを表形式で表示するためのものです。
実際には、表示される/しない というprop を受け取って、「表示するかしないか」を決めているのもこのコンポーネントです。
ここで行なっていることは、表のヘッダーを用意し、表のボディ部分にタスクリストの中に格納されたタスクを1個ずつmapして、Taskコンポーネントに渡しています。
Task.jsx
コードはこちら
TaskTableで「map」によって配列から1個ずつ取り出されたタスクをレンダリング(表示)していくためのコンポーネントです。
それぞれのタスクについて、完了チェックボックスやタイトル、詳細ボタン、編集ボタン、追加日、期日、削除ボタンを表示します。
TaskDetailModal.jsx
コードはこちら
Taskコンポーネントから各タスクの詳細情報を受け取り、モーダルで表示するためのコンポーネントです。
chakra UIライブラリのModalと、useDisclosureというカスタムフックを使って実装しました。
こういったライブラリを使うことで、何やら簡単に実装できるそうなのですが、いかんせん「正攻法」でやっていないので差がわからない…とはいえ、割とシンプルな記述でモーダルの処理が書けました。
TaskEditModal.jsx
コードはこちら
本当は詳細画面(TaskDetailModal)で編集までやりたかったのですが、なにぶん初心者ゆえ上手くいかず、
悪戦苦闘の末、別のモーダルとして編集画面を用意するという逃げの一手によって生まれたコンポーネントです。
最初に編集機能をつけようとしたとき、トップページのフォームを使って編集する感じの機能になって直感的ではなくなったので、モーダルにしました。
挙動としては、現在のタスク情報を取得し、フォームにデフォルト値としてセット。
編集して保存したとき、DB上のデータもきちんと書き換えられるようになりました。
さらにこの画面でのみ、詳細メモをつけられるようにしました。
苦労した点は、日付の既存データの取得です。
chakra UIの日付フォームの仕様なのか、DB上の日付をデフォルト値にセットするために日付フォーマットを整えてやる必要があったところです。
以下の部分ですね(抜粋)。
import { format } from 'date-fns';
const existingDueDate = format(task.dueDate, "yyyy-MM-dd");
const [dueDate, setDueDate] = useState(existingDueDate);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
useEffect(() => {
if (task) {
setName(task.name);
setDueDate(task.dueDate);
setMemo(task.memo);
}
}, [task]);
つまり、日付データを生のまま放り込んでも認識してくれないので、
まずformatコンポーネントをimportして、
formatを使って既存のtaskの期日を"yyyy-MM-dd"形式にしたものを
変数existingDueDateに格納し、
さらにそれをuseStateでdueDateを扱う時の初期値にした、
って感じです。
まわりくど〜〜〜〜〜〜い。けどこれがJavaScript…ッ
もっといいやり方あるのかな。
dateUtils.js
コードはこちら
これは別に要らなかった機能かもしれませんが、戯れで実装しました。
昨日は、タスク入力画面で、デフォルト値として「期日は明日」にしておくために、「今日の日付に1日足した値を計算するため」だけのコンポーネントです。
「formatが必要なために日付のデフォルト値が入らない問題」の経験を活かしてサクッと実装しました。
Rails (サーバーサイド)側
サーバーサイドのRailsは、非常にシンプルな記述で済みました。
こちらは特に書くことはないですね。
淡々とReactから受け取ったデータを、
保存したり表示したり消したり更新したり
するだけです。
ルーティング
Rails.application.routes.draw do
get "tasks" => "tasks#index"
post "tasks" => "tasks#create"
delete "tasks/:id" => "tasks#destroy"
put "tasks/:id" => "tasks#update"
end
DBカラム
タスク名
完了しているか(ステータス)
作成日
期日
メモ
コントローラー
コードはこちら
ごくごくシンプルなコントローラーです。
通常のRails と同様、ストロングパラメータを用いて受け取るparamsを指定しています。
苦労した点まとめ
今回は、ごくごくシンプルな教材のサンプルコードから自己流で改造しまくって、いろいろな機能をもりもりしてきました。
苦労したのは以下。
日付のフォーマットを正しく合わせないとフォームのデフォルト値にならない
1つの画面から複数のボタンでそれぞれモーダルを呼び出す方法がわからなかった→結果的に、useDisclosureを2つ書くことで解決
コンポーネントを切り出して、コードをシンプルに保つこと
(気づけばコードがどんどん伸びる。かといって適当に切り刻むと絡み合ってエライことになる)
まとめ
今回つくったアプリは、非常にシンプルな機能ですしスタイリングもほぼ行なっていません。でも、こんな簡単なアプリでもエライ大変なのだな…と痛感した1週間でした。
雛形ではコンポーネント分割もされていないような極小Appでしたが、ここまでコンポーネントを分割してimport/exportやpropの受け渡しなど、いろいろ書いたものをReactで作ったのは初めてで、とっても楽しかったです。
自分が初めて書いたコードを全世界中から見えるところに置いておくのは、以前の自分なら正気の沙汰ではないと思ったかもしれませんが
まぁ、学習の記録として残しておきましょう。
いつか、自分のコードを見返して笑ったり驚いたり、首をかしげたりするのも楽しそうです。
最後に、今回参考にした教材はこちらです。
今日はここまで。
最後までお読みいただきありがとうございました。