React+RailsAPIでSPAの処理の流れを解説してみた
目次[非表示]
- 1.はじめに
- 2.SPAとは
- 3.SPAの処理の流れを見てみよう
- 3.1.Todoアプリ
- 3.1.1.構成
- 3.1.2.事前準備
- 3.1.2.1.Dockerでコンテナを立ち上げる
- 3.1.2.2.CORS(Cross-Origin Resource Sharing)の設定
- 3.1.2.3.データベースの作成
- 3.2.Todoの一覧表示と保存処理
- 3.2.1.初回リクエストを送る
- 3.2.2.HTML,CSS,JavaScriptなどの静的ファイルを返す
- 3.2.3.Todoの一覧表示
- 3.2.3.1.todo_frontendの実装
- 3.2.4.todo_backendの実装
- 3.2.5.Todoの保存処理
- 3.2.5.1.todo_frontendの実装
- 3.2.5.2.todo_backendの実装
- 3.2.5.3.データベースを確認する
- 4.終わりに
- 5.採用情報
- 6.参考資料
はじめに
こんにちは。株式会社divxのエンジニア二階堂です。
昨今のWebアプリケーション開発では、フロントエンドとバックエンドを分けて開発することが増えており、その中の代表的な手法の1つである「SPA」があります。
僕自身、業務でSPAを開発する過程でフロントエンド、バックエンド、データベースの間でどのような処理や通信が行われているのか曖昧な理解で進めていたので、この機会に深堀りしたいと思い本記事を執筆しました。また、SPA開発はHowto系のコードだけの情報が多くそれだけの理解では難しかったので、処理の流れの図と共にコードを読み解きたいと思いました。
本記事では、まず「SPAとは何か」について説明します。
そして、実際にフロントエンドをReact、バックエンドをRailsのAPIを使用した簡単なTodoアプリを例にTodoの一覧表示とTodoを保存する処理の流れを図と共に挙動検証したりしながら解説します。
SPAの処理の流れを「なんとなく知っていた方」や「これから学びたい方」向けの内容なので、本記事をきっかけに今後のお役に立てれば幸いです。
SPAとは
SPAはシングルページアプリケーション(以下、SPA) の略で、Webアプリケーションの実装手法の一種です。
以下の図のように、ブラウザから初回リクエスト時のみ単一のWebページを読み込み、それ以降はJavaScript APIなどを通じて必要なデータのみをJSON形式などでサーバーから取得してJavaScriptで差分のみを更新します。
従来のWebページの読み込みでは、ユーザーが何らかの操作(ボタンやリンクのクリックなど)が行われるたびにサーバーは適切なWebページ全体の更新を行いますが、SPAでは1回のアクセスでWebページ全体を取得した後は、差分のみを表示させることでページの高速化やユーザーに軽快に動作させることが期待できます。
一方で、SEO などで不利になったり、開発コストがかかったり、状態の保守、操作の改善などの労力も必要になったりします。
SPAの処理の流れを見てみよう
SPAの仕組みがわかった上で、実際にフロントエンドをReact、バックエンドをRailsのAPIモードで簡単なTodoアプリを使用して流れを見ていきます。
Todoの一覧表示と保存処理の流れを動作確認や検証を挟みつつ解説していきます。
Todoアプリ
Todoの一覧表示と作成ができる簡易的なアプリケーションです。
以下、完成イメージになります。
※本記事では、Todoアプリを例にSPA構成の処理の流れを確認していくことが主題なので、1つ1つのコードの説明や「こっちの書き方や構成の方がいい」などのご意見もあるかと思いますが、今回は言及しません。
構成
以下、Todoアプリケーションの構成です。
フロントエンド
- React:18.0.17
- ChakraUI:2.3.2
- axios:0.27.2
バックエンド
- Ruby:3.1
- Ruby on Rails:7.0.1
- データベース
- MySQL:5.7
開発環境
- Docker Engine:20.10.12
- Docker Compose:1.29.2
- MacBook Air (M1, 2020)
- macOS Big Sur:11.6.4
事前準備
Dockerでコンテナを立ち上げる
フロントエンドのアプリケーションをtodo_frontend(React)
バックエンドのアプリケーションをtodo_backend(Rails API)※apiモードでRuby on Railsを構築 ※Dockerについての説明は割愛します。
※Dockerについての説明は割愛します。
CORS(Cross-Origin Resource Sharing)の設定
デフォルトでは、別のオリジンから別のオリジンのサーバーへのアクセス(GET, POSTなど)は制限されています。 よって、todo_frontendからのアクセスの許可をtodo_backendで設定します。
rack-corsというgemを使って、http://localhost:8000(todo_frontendで使用するオリジン)からの通信を許可します。
todo_backend/config/initializers/cors.rb
データベースの作成
todo_backendのdbコンテナ内にTodoを保存するtodosテーブルを作成しています。
titleカラムにTodo名が保存されます。
事前準備は以上です。
Todoの一覧表示と保存処理
Todoの一覧表示と保存処理の図をもとにどのような処理や通信が行われているのか解説します。
まず、以下、赤枠線の「初回リクエストを送る」と「HTML,CSS,JavaScriptなどの静的ファイルを返す」のリクエストとレスポンスは、初回のみの処理なので先に解説します。
初回リクエストを送る
初回はブラウザからWebサーバーに対してリクエストを送ります。
todo_frontendのコンテナが立ち上がっているのでhttp://localhost:8000からWebサーバーにリクエストを送ります。
HTML,CSS,JavaScriptなどの静的ファイルを返す
この時Webサーバーが返すHTML,CSS,JavaScriptは、todo_frontendでReactを使って作成したSPAの本体部分になります。
CSSフレームワークのChakraUIを使用してtodo_frontendでTodoが表示される部分や入力フォームなどの見た目を作成します。
ChakraUIをインストールし、ChakraProviderをAppコンポーネントを囲みます。
todo_frontend/frontend/src/index.jsx
Todoが表示されるTodoコンポーネントを作成します。
以下の赤枠の部分になります。(例として、「筋トレ」というTodoを追加した場合以下のように表示されます。)
todo_frontend/frontend/src/component/Todo.jsx
propsを使うことでコンポーネントに対してデータを渡すことができます。
Todoコンポーネントは、引数にpropsを受け取ることでtitle(Todo名)というデータを親コンポーネントから受け取れるようにしています。
次に、Todoの入力フォームを作成します。
以下の青枠の部分になります。
AppコンポーネントにTodoコンポーネントをimportします。
todo_frontend/frontend/src/App.jsx
Appコンポーネント内でTodoコンポーネントにpropsとしてtitleという値を渡しています。
そうすることでtitleがTodoコンポーネント内に渡されてTodo名として表示されます。
上記は例として、titleに筋トレという文字列を渡しています。
http://localhost:8000にリクエストするとtodo_frontend(React)で作成した見た目の部分がレスポンスされ、以下のように表示されます。
以上でSPAの見た目部分は完成です。
Todoの一覧表示
以降の処理はtodo_frontendとtodo_backendとのAPI通信になります。
todo_frontendからHTTPリクエストのGETメソッドでtodo_backendにリクエストを送って、データベースに保存されているTodoを参照して一覧を表示する機能です。
以下、赤枠線が処理の流れになります。
では、コードと処理の流れを解説します。
todo_frontendの実装
todo_frontendからtodo_backendにリクエストを送り、レスポンスされるTodoのJSONデータを受け取って一覧を表示させます。
axiosでリクエストを送ってデータを取得する
axiosを使ってデータの取得や追加を行います。
axiosとは、PromiseベースのHTTPクライアントです。HTTPリクエストのGETメソッドやPOSTメソッドなどを使ってサーバーからデータの取得、サーバーへの通信を通してデータの追加などができます。
Todoのデータを取得したいので、HTTPリクエストのGETメソッドを使ってtodo_backendにリクエストを送ります。
todo_frontend/frontend/src/App.jsx
それぞれ解説します。
axiosをインストールし、App.jsxにインポートします。
useStateでTodoの値とstateの更新用で関数を宣言します。
getTodosという関数を定義し、ここでaxiosを使いGETメソッドでHTTPリクエストを送りTodoの一覧を取得します。
GETメソッドの通信は、axios.getを使用します。
第一引数にパラメータ付きのURLを指定し、.then()で通信が成功した際の処理を書きます。 .catch()ではエラー時の処理を書きます。
axios.get("http://localhost:3001/api/todos")でtodo_backendとAPI通信を行っています。
.then()には通信成功時の処理を書きます。
レスポンスされるJSONデータは response.data の中に入っているので、setTodosの引数に指定しstate を更新します。
.catch()の中はエラー時にアラートを表示させる処理です。
mapメソッドで繰り返し処理をする
以下では、useStateで宣言したtodosをmapメソッドで繰り返し処理をして、差分を更新します。
Todoコンポーネントにpropsとして下記の値を渡しています。
key:Reactで配列処理する場合、配列内の項目に安定した識別性を与えるためkeyという属性に値を渡す必要があります。todoのidをkeyとして指定しています。
title:Todo名を渡しています。
そして、useEffectの中でgetTodos関数を実行し、第二引数に[]を指定することで、コンポーネントの初回のレンダリング時にTodoの一覧を取得します。
todo_backendの実装
ルーティングを設定する
todo_frontendからGETメソッドでhttp://localhost:3001/api/todosにリクエストがきたら、todosコントローラーのindexアクションで処理を行うようにルーティングを設定します。
todo_backend/config/routes.rb
indexアクションを追加する
todosコントローラーにindexアクションを定義します。
todo_backend/app/controllers/api/todos_controller.rb
indexアクション内の処理は、allメソッドでtodosテーブル内のレコード、つまりTodoの一覧を全て取得し、それをtodosという変数に格納しています。そして、render json: todosでJSONデータとしてtodo_frontendにレスポンスする処理を行っています。
検証として、テストデータをデータベースに作成してTodoの一覧が取得できJSONでレスポンスされるか確認してみます。
Todoのテストデータを3つ作成します。
todo_backend/db/seeds.rb
apiコンテナに入ってrails db:seedコマンドでテストデータを作成します。
curlコマンドでGETメソッドを指定し、http://localhost:3001/api/todosからJSONが返るか確認します。
コマンドを実行すると、以下のようにJSON形式で出力されたことがわかります。
実際にJSONデータが出力できたので、todo_frontendでこのTodoのテストデータを受け取り画面に表示されれば正常です。
レスポンス成功時にの処理内にconsole.log(response.data);を仕込んでTodoのテストデータが取得できているのか確認してみます。
todo_frontend/frontend/src/App.jsx
検証ツールを開きコンソールを確認すると、todo_backendからJSONでレスポンスされたTodoのテストデータを取得できています。
http://localhost:8000にアクセスするとTodoのテストデータが表示されました。
以上でTodoの一覧表示は完成です。
Todoの保存処理
todo_frontendからHTTPリクエストのPOSTメソッドでtodo_backendにリクエストを送って、入力したTodoをデータベースに保存します。
以下、赤枠線が処理の流れになります。
では、コードと処理の流れを解説します。
todo_frontendの実装
axiosでリクエストを送ってデータを追加する
フォームに入力したTodoをtodo_backendに送る処理です。
todo_frontend/frontend/src/App.tsx
追加したコードをそれぞれ解説します。
フォームに入力された値を管理するstateです。
Inputタグのvalueにtitleの値を参照させます。
onChangeイベントでtitleの値が変更した時に、Inputの値を引数としてsetTitleを実行しています。それによって、フォームに入力された値が変わるたびにstateのtitleがsetTitleで更新されます。
「追加」ボタンがクリックされたら、onClickイベントでcreateTodo関数を実行させます。
Todoを作成するcreateTodoという関数を定義します。
axiosを使ってHTTPリクエストのPOSTメソッドでhttp://localhost:3001/api/todosにtitleを付与してリクエストを送っています。
このtitleは、前述したconst [title, setTitle] = useState("");のstateです。
フォームの値が変更されるたびに、stateのtitleがsetTitleで更新されるので、titleとして送信されることになります。このtitleがtodo_backendのtodosコントローラーのparamsに入るようなイメージです。
リクエストが成功したら.then()の中で、setTitle("");を実行することでフォームの値を空にし、getTodos();を実行することで、作成したTodoを含めた一覧を再取得しています。
.catch()の中はエラー時にアラートを表示させる処理です。
todo_backendの実装
ルーティングを設定する
todo_frontendからPOSTメソッドでhttp://localhost:3001/api/todosにリクエストがきたら、todosコントローラーのcreateアクションで処理を行うようにルーティングを設定します。
todo_backend/config/routes.rb
createアクションを追加する
todosコントローラーにcreateアクションを定義します。
todo_backend/app/controllers/api/todos_controller.rb
それぞれ解説します。
createアクションの中でtodo_frontendから送られてくるパラメーターをtodo_paramsとして引数に指定しています。
保存処理時に条件分岐を書いてます。
成功したら、ステータスコード200番とtodoのJSONデータを返します。
失敗したら、ステータスコード500番とエラーメッセージのJSONデータを返します。
実際にtodo_frontendから値が送られているのか、createアクション内にbinding.pryを仕込んで確認してみます。
docker attachでtodo_backend_apiコンテナにアタッチします。
検証として、「筋トレ」と入力して追加ボタンをクリックすると止まるので、確認してみます。
todo_frontendから送られてきたparamsのtitleの値が「筋トレ」となっています。
これでtodo_frontendからtodo_backendに正常な値が送られていることが確認できました。
exitで終了すると、todoテーブルに保存されます。
その後、todo_frontendのcreateTodoではリクエストに成功したらgetTodosが実行されます。
よって、保存が完了したらindexアクション内のtodoテーブルに保存されているTodoの一覧を取得する処理が行われているのがわかります。
データベースを確認する
データベースに正常に値が保存されているかdbコンテナに入って、todoテーブルを確認してみます。
todoテーブルにTodo(筋トレ)が保存されているのがわかります。
また、検証ツールのNetWorkタブで保存処理成功時のrender json: { status: 200, todo: todo }がレスポンスされているか確認できました。
バリデーションを設定する
空入力のときには保存されないようにバリデーションを設定してみます。
todo_backend/app/models/todo.rb
検証として、フォームを空のまま追加ボタンを押すとバリデーションで引っ掛かりリクエストに失敗します。
検証ツールのNetWorkタブで保存処理失敗時のrender json: { status: 500, message: "タスクの作成に失敗しました" }のレスポンスが確認できました。
以上でTodoの保存機能は完成です。
最後に1通り挙動確認をしてみます。
検証として、「プログラミング」と「掃除」のTodoを追加してみます。
再度、getTodosの中にconsole.log(response.data);を仕込んで確認すると、正常にJSONデータがレスポンスされています。
dbコンテナ内にも正常に保存されています。
以上で、Todoの一覧表示と保存処理は完成しました。
終わりに
今回はSPAの処理の図を書き、実際にその通りの流れになるのかをTodoアプリを使用して検証してみました。
図を書くことで頭の中でコードだけを追っていくよりもはるかに自分自身の理解の手助けになりました。以前、Ruby on RailsのMVCモデルのイメージがつかなかった時に図で説明を受けたときに、パズルのピースがカチッとはまったような感覚があったからです。今後も何かしら詰まった時や理解し難い時などには、簡単にでも図を書いてみようと思います。
そして、基礎的ではありますがconsole.logやbinding.pryを使用することで、何の値がどのタイミングでリクエストやレスポンスが行われているのか目視できます。それによって詰まった時にどこが原因なのか横着せずに調べる癖を付けられるようになりました。基礎的なことを忘れずに今後も引き続き活用していきたいと思います。
最後まで読んでいただきありがとうございました。
採用情報
株式会社divxでは一緒に働ける仲間を募集しています。
興味があるかたはぜひ採用ページを御覧ください。
参考資料
- https://developer.mozilla.org/ja/docs/Glossary/SPA
- https://developer.mozilla.org/ja/docs/Web/HTTP/CORS
- https://chakra-ui.com/
- https://developer.mozilla.org/ja/docs/Web/HTTP/Methods
- https://developer.mozilla.org/ja/docs/Glossary/JSON
- https://github.com/axios/axios
- https://ja.reactjs.org/docs/lists-and-keys.html
- https://ja.reactjs.org/docs/forms.html
- https://docs.docker.jp/engine/reference/commandline/attach.html