著者は、プログラムの一環として寄付を受け取る団体として、Electronic Frontier Foundationを選択しました。
はじめに
Ruby on Railsは人気のあるサーバーサイドのWebアプリケーションフレームワークです。これは、GitHub、Basecamp、SoundCloud、Airbnb、およびTwitchなど、現在ウェブ上に存在する多くの人気のあるアプリケーションを動かしています。プログラマーの経験とそれを取り巻く情熱的なコミュニティに重点を置いており、Ruby on Railsは、現代のWebアプリケーションを構築および維持するために必要なツールを提供します。
Reactは、フロントエンドのユーザーインターフェイスを作成するために使用されるJavaScriptライブラリです。Facebookに支持されており、現在ウェブ上で最も人気のあるフロントエンドライブラリの1つです。Reactには、仮想DOM(virtual Document Object Model (DOM))、コンポーネントアーキテクチャ、および状態管理などの機能があり、フロントエンド開発のプロセスをより整理されて効率的にします。
Webのフロントエンドがサーバーサイドのコードとは別のフレームワークに移行する中で、RailsのエレガンスとReactの効率を組み合わせることで、最新のトレンドに基づいた強力でモダンなアプリケーションを構築することができます。Railsのテンプレートエンジンではなく、Reactを使用してコンポーネントをRailsのビューからレンダリングすることで、JavaScriptとフロントエンド開発の最新の進歩を活用しながら、Ruby on Railsの表現力を活かすことができます。
このチュートリアルでは、お気に入りのレシピを保存し、Reactのフロントエンドで表示するRuby on Railsアプリケーションを作成します。作業が完了すると、Bootstrapでスタイル付けされたReactインターフェースを使用して、レシピを作成、表示、削除することができるようになります。
前提条件
このチュートリアルを進めるには、以下が必要です:
-
Node.jsおよびnpmが開発マシンにインストールされています。このチュートリアルでは、Node.jsバージョン16.14.0とnpmバージョン8.3.1を使用しています。Node.jsは、ブラウザの外でコードを実行することができるJavaScriptランタイム環境です。Node.jsには、npmと呼ばれる事前にインストールされたパッケージマネージャが付属しており、パッケージのインストールや更新を行うことができます。これらをUbuntu 20.04またはmacOSにインストールするには、Ubuntu 20.04にNode.jsをインストールする方法の「PPAを使用してインストールする」セクションに従うか、macOSにNode.jsをインストールしてローカル開発環境を作成する方法の手順に従ってください。
-
開発マシンにはYarnパッケージマネージャがインストールされており、これによりReactフレームワークをダウンロードできます。このチュートリアルはバージョン1.22.10でテストされました。この依存関係をインストールするには、公式のYarnインストールガイドに従ってください。
-
Ruby on Railsがインストールされました。これを取得するには、Ubuntu 20.04でrbenvを使用してRuby on Railsをインストールする方法ガイドに従ってください。このアプリケーションをmacOSで開発したい場合は、macOSでrbenvを使用してRuby on Railsをインストールする方法をご利用いただけます。このチュートリアルはRubyのバージョン3.1.2とRailsのバージョン7.0.4でテストされましたので、インストールプロセス中にこれらのバージョンを指定してください。
注意: Railsのバージョン7は後方互換性がありません。Railsのバージョン5を使用している場合は、Ubuntu 18.04でReactフロントエンド付きのRuby on Rails v5プロジェクトを設定する方法のチュートリアルを参照してください。
- PostgreSQLをインストールしました。ステップ1と2で説明されているように、Ubuntu 20.04でのPostgreSQLのRuby on Railsアプリケーションの使用方法またはmacOSでのPostgreSQLのRuby on Railsアプリケーションの使用方法に従います。このチュートリアルに従うには、PostgreSQLのバージョン12以上を使用できます。別のLinuxディストリビューションや他のOSでこのアプリケーションを開発したい場合は、公式のPostgreSQLダウンロードページを参照してください。PostgreSQLの使用方法についての詳細は、PostgreSQLのインストールと使用方法を参照してください。
ステップ1 — 新しいRailsアプリケーションの作成
このステップでは、Railsアプリケーションフレームワークでレシピアプリケーションを構築します。まず、Reactと連携する新しいRailsアプリケーションを作成します。
Railsは、モダンなWebアプリケーションを構築するために必要なすべてを作成するジェネレーターと呼ばれるいくつかのスクリプトを提供しています。これらのコマンドとそれらが行うことの完全なリストを確認するには、ターミナルで次のコマンドを実行してください:
- rails -h
このコマンドは、アプリケーションのパラメータを設定するための包括的なオプションリストを生成します。リストされているコマンドの1つは、new
コマンドで、これにより新しいRailsアプリケーションが作成されます。
次に、new
ジェネレータを使用して新しいRailsアプリケーションを作成します。ターミナルで次のコマンドを実行します:
- rails new rails_react_recipe -d postgresql -j esbuild -c bootstrap -T
前述のコマンドは、rails_react_recipe
というディレクトリに新しいRailsアプリケーションを作成し、必要なRubyおよびJavaScriptの依存関係をインストールし、Webpackを構成します。このnew
ジェネレータコマンドに関連付けられているフラグには、次のものがあります:
-d
フラグは、この場合にはPostgreSQLが好ましいデータベースエンジンを指定します。-j
フラグは、アプリケーションのJavaScriptアプローチを指定します。Railsは、Railsアプリケーション内のJavaScriptコードを処理するいくつかの異なる方法を提供しています。-j
フラグに渡されるesbuild
オプションは、-j
フラグにesbuildを優先するJavaScriptバンドラとして事前に設定するようRailsに指示します。-c
フラグは、アプリケーションのCSSプロセッサを指定します。この場合、Bootstrapが優先されます。-T
フラグは、このチュートリアルでテストを書かないため、テストファイルの生成をスキップするようRailsに指示します。このコマンドは、Railsが提供するテストツールとは異なるRubyのテストツールを使用したい場合にも推奨されます。
コマンドが完了したら、アプリのルートディレクトリであるrails_react_recipe
ディレクトリに移動します:
- cd rails_react_recipe
次に、ディレクトリの内容をリストします:
- ls
OutputGemfile README.md bin db node_modules storage yarn.lock
Gemfile.lock Rakefile config lib package.json tmp
Procfile.dev app config.ru log public vendor
このルートディレクトリには、Railsアプリケーションの構造を構成するいくつかの自動生成されたファイルとフォルダがあります。これには、Reactアプリケーションの依存関係を含むpackage.json
ファイルが含まれています。
新しいRailsアプリケーションを正常に作成したので、次のステップではデータベースに接続します。
ステップ2 — データベースの設定
新しいRailsアプリケーションを実行する前に、まずデータベースに接続する必要があります。このステップでは、新しく作成したRailsアプリケーションをPostgreSQLデータベースに接続し、レシピデータを必要に応じて保存および取得します。
config/database.yml
にあるdatabase.yml
ファイルには、異なる開発環境のデータベース名などのデータベースの詳細が含まれています。Railsは、各開発環境にデータベース名を環境名の後にアンダースコア(_
)を付けて指定します。このチュートリアルでは、デフォルトのデータベース構成値を使用しますが、必要に応じて構成値を変更できます。
注意:この時点で、Railsがデータベースを作成するために使用するPostgreSQLのロールを設定するためにconfig/database.yml
を変更することができます。事前条件として、パスワードで保護されたロールを作成しましたRuby on RailsアプリケーションでPostgreSQLを使用する方法チュートリアルで。まだユーザーを設定していない場合は、同じ事前条件チュートリアルのステップ4 — データベースの設定と作成の指示に従うことができます。
Railsは、create
、drop
、reset
などのデータベース作業に役立つ多くのコマンドを提供しています。アプリケーションのデータベースを作成するには、ターミナルで以下のコマンドを実行します:
- rails db:create
このコマンドはdevelopment
とtest
のデータベースを作成し、次の出力を生成します:
OutputCreated database 'rails_react_recipe_development'
Created database 'rails_react_recipe_test'
アプリケーションがデータベースに接続されたので、次のコマンドを実行してアプリケーションを起動します:
- bin/dev
Railsはbin/dev
スクリプトの代替手段を提供しており、アプリのルートディレクトリにあるProcfile.dev
ファイルのコマンドをForemanジェムを使用して実行することにより、Railsアプリケーションを起動します。
このコマンドを実行すると、コマンドプロンプトが消え、その代わりに次の出力が印刷されます:
Outputstarted with pid 70099
started with pid 70100
started with pid 70101
yarn run v1.22.10
yarn run v1.22.10
$ esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets --watch
$ sass ./app/assets/stylesheets/application.bootstrap.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules --watch
=> Booting Puma
=> Rails 7.0.4 application starting in development
=> Run `bin/rails server --help` for more startup options
[watch] build finished, watching for changes...
Puma starting in single mode...
* Puma version: 5.6.5 (ruby 3.1.2-p20) ("Birdie's Version")
* Min threads: 5
* Max threads: 5
* Environment: development
* PID: 70099
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop
Sass is watching for changes. Press Ctrl-C to stop.
アプリケーションにアクセスするには、ブラウザウィンドウを開いてhttp://localhost:3000
に移動します。Railsのデフォルトのウェルカムページが読み込まれます。これは、Railsアプリケーションを正しく設定したことを意味します:
ウェブサーバーを停止するには、サーバーが実行されている端末でCTRL+C
を押します。Pumaからさようならメッセージが表示されます:
Output^C SIGINT received, starting shutdown
- Gracefully stopping, waiting for requests to finish
=== puma shutdown: 2019-07-31 14:21:24 -0400 ===
- Goodbye!
Exiting
sending SIGTERM to all processes
terminated by SIGINT
terminated by SIGINT
exited with code 0
その後、ターミナルプロンプトが再表示されます。
料理レシピアプリケーションのデータベースの設定が成功しました。次のステップでは、Reactフロントエンドを構築するために必要なJavaScriptの依存関係をインストールします。
ステップ3 — フロントエンドの依存関係のインストール
このステップでは、料理レシピアプリケーションのフロントエンドで必要なJavaScriptの依存関係をインストールします。それらには以下が含まれます:
- ユーザーインターフェースを構築するためのReact
- React DOM:ReactがブラウザのDOMとやり取りするためのもの。
- React Router:Reactアプリケーションでのナビゲーションを処理するためのもの。
これらのパッケージをYarnパッケージマネージャーでインストールするには、次のコマンドを実行します:
- yarn add react react-dom react-router-dom
このコマンドは、指定されたパッケージをインストールし、それらをpackage.json
ファイルに追加するためにYarnを使用します。これを確認するには、プロジェクトのルートディレクトリにあるpackage.json
ファイルを開きます:
- nano package.json
インストールされたパッケージはdependencies
キーの下にリストされます:
{
"name": "app",
"private": "true",
"dependencies": {
"@hotwired/stimulus": "^3.1.0",
"@hotwired/turbo-rails": "^7.1.3",
"@popperjs/core": "^2.11.6",
"bootstrap": "^5.2.1",
"bootstrap-icons": "^1.9.1",
"esbuild": "^0.15.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.3.0",
"sass": "^1.54.9"
},
"scripts": {
"build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets",
"build:css": "sass ./app/assets/stylesheets/application.bootstrap.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules"
}
}
ファイルをCTRL+X
で閉じます。
いくつかのフロントエンド依存関係をアプリケーションにインストールしました。次に、食事レシピアプリケーションのホームページを設定します。
ステップ4 — ホームページの設定
必要な依存関係がインストールされたので、アプリケーションのホームページを作成し、ユーザーがアプリケーションを最初に訪れたときのランディングページとして機能させます。
RailsはアプリケーションのためにModel-View-Controllerアーキテクチャパターンに従います。MVCパターンでは、コントローラーの目的は特定のリクエストを受け取り、それらを適切なモデルまたはビューに渡すことです。現在、ルートURLがブラウザでロードされると、アプリケーションはRailsのウェルカムページを表示します。これを変更するには、ホームページのためのコントローラーとビューを作成し、それをルートにマッチさせます。
Railsは、コントローラを作成するためのcontroller
ジェネレータを提供します。このcontroller
ジェネレータは、コントローラ名と対応するアクションを受け取ります。これについて詳しくは、Railsのドキュメントを参照してください。
このチュートリアルでは、コントローラをHomepage
と呼びます。次のコマンドを実行して、Homepage
コントローラとindex
アクションを作成します:
- rails g controller Homepage index
注意:
Linuxでは、エラーFATAL: Listen error: unable to monitor directories for changes.
が、システムの制限により、マシンが監視できるファイルの数を超えた場合に発生する場合があります。これを修正するには、次のコマンドを実行します:
- echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
このコマンドは、Listen
で監視できるディレクトリの数を524288
に永久的に増やします。再度同じコマンドを実行し、524288
をご希望の数値に置き換えることで、これを変更できます。
controller
コマンドを実行すると、次のファイルが生成されます:
- A
homepage_controller.rb
file for receiving all homepage-related requests. This file contains theindex
action you specified in the command. - A
homepage_helper.rb
file for adding helper methods related to theHomepage
controller. - ホームページに関連するものをレンダリングするための
index.html.erb
ファイル。
Railsコマンドを実行して作成されたこれらの新しいページに加えて、Railsはconfig/routes.rb
にあるルートファイルを更新し、ホームページ用のget
ルートを追加します。これはルートルートとして変更します。
A root route in Rails specifies what will show up when users visit the root URL of your application. In this case, you want your users to see your homepage. Open the routes file located at config/routes.rb
in your favorite editor:
- nano config/routes.rb
このファイルでは、get 'homepage/index'
をroot 'homepage#index'
に置き換えて、ファイルが次のように一致するようにします:
Rails.application.routes.draw do
root 'homepage#index'
# このファイル内で利用可能なDSLの詳細については、http://guides.rubyonrails.org/routing.html を参照してください。
end
この修正は、Railsにリクエストをアプリケーションのルートからindex
アクションのHomepage
コントローラーにマップするように指示します。そして、ブラウザでindex.html.erb
ファイルがある場所にある内容をレンダリングします。app/views/homepage/index.html.erb
。
ファイルを保存して閉じます。
これが機能していることを確認するには、アプリケーションを起動します:
- bin/dev
ブラウザでアプリケーションを開いたり更新したりすると、アプリケーションの新しいランディングページが読み込まれます:
アプリケーションが動作していることを確認したら、サーバーを停止するにはCTRL+C
を押します。
次に、~/rails_react_recipe/app/views/homepage/index.html.erb
ファイルを開きます:
- nano ~/rails_react_recipe/app/views/homepage/index.html.erb
ファイル内のコードを削除して、空のファイルとして保存します。これにより、index.html.erb
の内容がフロントエンドのReactのレンダリングと干渉しないようにします。
これでアプリケーションのホームページの設定が完了しました。次のセクションに移動し、アプリケーションのフロントエンドをReactで使用するように構成します。
ステップ5 — RailsフロントエンドとしてReactを構成する
このステップでは、Railsを設定して、アプリケーションのフロントエンドにテンプレートエンジンの代わりにReactを使用します。この新しい設定により、Reactを使用してよりビジュアルに魅力的なホームページを作成することができます。
Railsアプリケーションの生成時に指定されたesbuild
オプションの助けを借りて、JavaScriptがRailsとシームレスに連携するために必要なほとんどのセットアップは既に完了しています。残りの作業は、Reactアプリケーションのエントリーポイントをesbuild
のJavaScriptファイルのエントリーポイントにロードすることです。これを行うには、まずapp/javascript
ディレクトリにcomponents
ディレクトリを作成します。
- mkdir ~/rails_react_recipe/app/javascript/components
components
ディレクトリには、ホームページのコンポーネントと、アプリケーション内の他のReactコンポーネント(Reactアプリケーションへのエントリーファイルを含む)が格納されます。
次に、app/javascript/application.js
にあるapplication.js
ファイルを開きます。
- nano ~/rails_react_recipe/app/javascript/application.js
以下のコードをファイルに追加します。
// パッケージのpackage.jsonのビルドスクリプトのエントリーポイント
import "@hotwired/turbo-rails"
import "./controllers"
import * as bootstrap from "bootstrap"
import "./components"
application.js
ファイルに追加されたコードは、エントリーのindex.jsx
ファイルのコードをインポートし、esbuild
でバンドルできるようにします。RailsアプリのJavaScriptエントリーポイントに/components
ディレクトリがインポートされたので、ホームページのためのReactコンポーネントを作成できます。ホームページにはテキストと、すべてのレシピを表示するためのアクションボタンが含まれます。
ファイルを保存して閉じます。
次に、components
ディレクトリにHome.jsx
ファイルを作成します。
- nano ~/rails_react_recipe/app/javascript/components/Home.jsx
以下のコードをファイルに追加します。
import React from "react";
import { Link } from "react-router-dom";
export default () => (
<div className="vw-100 vh-100 primary-color d-flex align-items-center justify-content-center">
<div className="jumbotron jumbotron-fluid bg-transparent">
<div className="container secondary-color">
<h1 className="display-4">Food Recipes</h1>
<p className="lead">
A curated list of recipes for the best homemade meal and delicacies.
</p>
<hr className="my-4" />
<Link
to="/recipes"
className="btn btn-lg custom-button"
role="button"
>
View Recipes
</Link>
</div>
</div>
</div>
);
このコードでは、ReactとReact RouterからLink
コンポーネントをインポートします。 Link
コンポーネントは、ページ間を移動するためのハイパーリンクを作成します。 その後、ホームページのためのいくつかのマークアップ言語を含む関数コンポーネントを作成し、Bootstrapクラスでスタイルを付けてエクスポートします。
ファイルを保存して閉じます。
Home
コンポーネントが設定されたら、React Routerを使用してルーティングを設定します。 app/javascript
ディレクトリにroutes
ディレクトリを作成します:
- mkdir ~/rails_react_recipe/app/javascript/routes
routes
ディレクトリには、いくつかのルートとそれに対応するコンポーネントが含まれます。 指定されたルートがロードされるたびに、対応するコンポーネントがブラウザにレンダリングされます。
routes
ディレクトリに、index.jsx
ファイルを作成します:
- nano ~/rails_react_recipe/app/javascript/routes/index.jsx
次のコードを追加します:
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "../components/Home";
export default (
<Router>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</Router>
);
このindex.jsx
ルートファイルでは、次のモジュールをインポートします:Reactを使用するためのReact
モジュール、およびReact RouterからのBrowserRouter
、Routes
、およびRoute
モジュール。 これらは、異なるルート間を移動するのに役立ちます。 最後に、ルート(/
)に一致するリクエストがあるたびにレンダリングされるHome
コンポーネントをインポートします。
ファイルを保存して終了します。
React Routerを使用してルーティングを設定しました。 Reactが利用可能なルートを認識して使用するためには、これらのルートをアプリケーションのエントリーポイントで利用できる必要があります。 これを実現するには、Reactがエントリーファイルでレンダリングするコンポーネント内でルートをレンダリングします。
app/javascript/components
ディレクトリにApp.jsx
ファイルを作成してください:
- nano ~/rails_react_recipe/app/javascript/components/App.jsx
App.jsx
ファイルに次のコードを追加します:
import React from "react";
import Routes from "../routes";
export default props => <>{Routes}</>;
App.jsx
ファイルでは、Reactとさきほど作成したルートファイルをインポートします。 その後、ルートをレンダリングするコンポーネントをエクスポートします。フラグメント内で。 このコンポーネントは、アプリケーションが読み込まれるたびに、アプリケーションのエントリーポイントでレンダリングされます。
ファイルを保存して閉じます。
App.jsx
を設定したので、エントリーファイルでレンダリングできます。 components
ディレクトリにindex.jsx
ファイルを作成してください:
- nano ~/rails_react_recipe/app/javascript/components/index.jsx
index.js
ファイルに次のコードを追加します:
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
document.addEventListener("turbo:load", () => {
const root = createRoot(
document.body.appendChild(document.createElement("div"))
);
root.render(<App />);
});
import
行で、Reactライブラリ、ReactDOMからcreateRoot
関数、およびApp
コンポーネントをインポートします。 ReactDOMのcreateRoot
関数を使用して、ページに追加されたdiv
要素としてルート要素を作成し、その中にApp
コンポーネントをレンダリングします。 アプリケーションが読み込まれると、ReactはApp
コンポーネントのコンテンツをページ上のdiv
要素内にレンダリングします。
ファイルを保存して閉じます。
最後に、ホームページにいくつかのCSSスタイルを追加します。
`application.bootstrap.scss
`ファイルを~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss
ディレクトリで開いてください:
- nano ~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss
次に、application.bootstrap.scss
ファイルの内容を以下のコードで置き換えてください:
@import 'bootstrap/scss/bootstrap';
@import 'bootstrap-icons/font/bootstrap-icons';
.bg_primary-color {
background-color: #FFFFFF;
}
.primary-color {
background-color: #FFFFFF;
}
.bg_secondary-color {
background-color: #293241;
}
.secondary-color {
color: #293241;
}
.custom-button.btn {
background-color: #293241;
color: #FFF;
border: none;
}
.hero {
width: 100vw;
height: 50vh;
}
.hero img {
object-fit: cover;
object-position: top;
height: 100%;
width: 100%;
}
.overlay {
height: 100%;
width: 100%;
opacity: 0.4;
}
ページのためにカスタムの色を設定します。.hero
セクションは、ウェブサイトのトップページに大きなウェブバナーまたはヒーローイメージのフレームワークを作成します。また、custom-button.btn
は、ユーザーがアプリケーションに入るために使用するボタンにスタイルを付けます。
CSSスタイルが設定されたら、ファイルを保存して終了します。
次に、アプリケーションのウェブサーバーを再起動してください:
- bin/dev
その後、ブラウザでアプリケーションをリロードしてください。新しいホームページがロードされます:
ウェブサーバーをCTRL+C
で停止してください。
このステップで、アプリケーションをフロントエンドとしてReactを使用するように構成しました。次のステップでは、レシピを作成、読み取り、更新、削除するためのモデルとコントローラーを作成します。
ステップ6 — レシピコントローラーとモデルの作成
あなたのアプリケーションにReactフロントエンドを設定したので、次にレシピモデルとコントローラーを作成します。
レシピモデルを作成するには、Railsが提供するgenerate model
サブコマンドを使用し、モデルの名前とそのカラム及びデータ型を指定して、以下のコマンドを実行します:
- rails generate model Recipe name:string ingredients:text instruction:text image:string
前述のコマンドはRailsにRecipe
モデルをname
カラム(型はstring
)、ingredients
とinstruction
カラム(型はtext
)、そしてimage
カラム(型はstring
)と共に作成するよう指示します。このチュートリアルでは、モデルにRecipe
という名前を付けています。なぜならRailsのモデルは単数形の名前を使用し、それに対応するデータベーステーブルは複数形の名前を使用するからです。
generate model
コマンドを実行すると、2つのファイルが作成され、以下の出力が表示されます:
Output invoke active_record
create db/migrate/20221017220817_create_recipes.rb
create app/models/recipe.rb
作成された2つのファイルは:
- A
recipe.rb
file that holds all the model-related logic. - A
20221017220817_create_recipes.rb
file (the number at the beginning of the file may differ depending on the date when you run the command). This migration file contains the instruction for creating the database structure.
次に、データベースに保存されるデータが有効であることを確認するために、レシピモデルファイルを編集します。これは、モデルにいくつかのデータベースバリデーションを追加することで達成できます。
app/models/recipe.rb
にあるレシピモデルを開きます:
- nano ~/rails_react_recipe/app/models/recipe.rb
次のコード行をファイルに追加してください:
class Recipe < ApplicationRecord
validates :name, presence: true
validates :ingredients, presence: true
validates :instruction, presence: true
end
このコードでは、モデル検証を追加し、name
、ingredients
、およびinstruction
フィールドの存在をチェックします。これらの三つのフィールドがないと、レシピは無効になり、データベースに保存されません。
ファイルを保存して閉じてください。
データベース内にrecipes
テーブルを作成するために、Railsでマイグレーションを実行する必要があります。これはデータベースにプログラムで変更を加える方法です。マイグレーションが設定したデータベースで動作するようにするために、20221017220817_create_recipes.rb
ファイルを変更する必要があります。
エディタでこのファイルを開いてください:
- nano ~/rails_react_recipe/db/migrate/20221017220817_create_recipes.rb
次のようにファイルを一致させるために、ハイライトされた素材を追加してください:
class CreateRecipes < ActiveRecord::Migration[5.2]
def change
create_table :recipes do |t|
t.string :name, null: false
t.text :ingredients, null: false
t.text :instruction, null: false
t.string :image, default: 'https://raw.githubusercontent.com/do-community/react_rails_recipe/master/app/assets/images/Sammy_Meal.jpg'
t.timestamps
end
end
end
このマイグレーションファイルには、change
メソッドを持つRubyクラスと、recipes
というテーブルを作成するコマンドが含まれています。列とそのデータ型も含まれています。また、20221017220817_create_recipes.rb
にnull: false
を追加して、name
、ingredients
、およびinstruction
列にNOT NULL
制約を追加し、これらの列がデータベースを変更する前に値を持っていることを確認します。最後に、イメージ列のデフォルトのイメージURLを追加します。別のイメージを使用したい場合は、別のURLを使用できます。
これらの変更で、ファイルを保存して終了します。これで、マイグレーションを実行してテーブルを作成する準備が整いました。ターミナルで次のコマンドを実行してください:
- rails db:migrate
データベースのマイグレーションコマンドを使用して、マイグレーションファイル内の指示を実行します。コマンドが成功裏に実行されると、次のような出力が表示されます:
Output== 20190407161357 CreateRecipes: migrating ====================================
-- create_table(:recipes)
-> 0.0140s
== 20190407161357 CreateRecipes: migrated (0.0141s) ===========================
レシピモデルを設定したら、次にレシピコントローラーを作成して、レシピの作成、読み取り、削除のロジックを追加します。次のコマンドを実行します:
- rails generate controller api/v1/Recipes index create show destroy --skip-template-engine --no-helper
このコマンドでは、api/v1
ディレクトリにRecipes
コントローラーを作成し、index
、create
、show
、destroy
アクションを追加します。index
アクションは、すべてのレシピを取得する処理を担当し、create
アクションは新しいレシピを作成する責任があり、show
アクションは単一のレシピを取得し、destroy
アクションはレシピを削除するロジックを保持します。
コントローラーをより軽量にするために、いくつかのフラグを渡します:
--skip-template-engine
は、Reactがフロントエンドのニーズを処理するため、Railsビューファイルの生成をスキップするようにRailsに指示します。--no-helper
は、コントローラー用のヘルパーファイルの生成をスキップするようにRailsに指示します。
コマンドを実行すると、Recipes
コントローラーの各アクションに対するルートがルートファイルに更新されます。
コマンドを実行すると、このような出力が表示されます:
Output create app/controllers/api/v1/recipes_controller.rb
route namespace :api do
namespace :v1 do
get 'recipes/index'
get 'recipes/create'
get 'recipes/show'
get 'recipes/destroy'
end
end
これらのルートを使用するために、config/routes.rb
ファイルを変更します。routes.rb
ファイルをテキストエディターで開きます:
- nano ~/rails_react_recipe/config/routes.rb
次のコードのようにこのファイルを更新し、強調表示された行を変更または追加します:
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
get 'recipes/index'
post 'recipes/create'
get '/show/:id', to: 'recipes#show'
delete '/destroy/:id', to: 'recipes#destroy'
end
end
root 'homepage#index'
get '/*path' => 'homepage#index'
# https://guides.rubyonrails.org/routing.html で定義されたDSLに従ってアプリケーションのルートを定義します。
# ルートパス ("/") を定義します。
# root "articles#index"
end
このルートファイルでは、create
とdestroy
のルートのHTTP動詞を変更し、データをpost
およびdelete
するようにします。また、show
とdestroy
アクションのルートに:id
パラメータを追加して、読み取りまたは削除するレシピの識別番号を保持します。
次に、既存のルートと一致しない他のリクエストをhomepage
コントローラーのindex
アクションにリダイレクトするget '/*path'
のキャッチオールルートを追加します。フロントエンドのルーティングは、レシピの作成、読み取り、または削除と関係のないリクエストを処理します。
ファイルを保存して終了します。
アプリケーションで利用可能なルートの一覧を評価するには、次のコマンドを実行します:
- rails routes
このコマンドを実行すると、プロジェクトのURIパターン、動詞、および一致するコントローラーまたはアクションの長い一覧が表示されます。
次に、一度にすべてのレシピを取得するためのロジックを追加します。Railsは、ActiveRecordライブラリを使用して、このようなデータベース関連のタスクを処理します。ActiveRecordはクラスを関係データベーステーブルに接続し、それらとの作業に対する豊富なAPIを提供します。
すべてのレシピを取得するために、ActiveRecordを使用してレシピテーブルをクエリし、データベース内のすべてのレシピを取得します。
次のコマンドで`recipes_controller.rb`ファイルを開きます:
- nano ~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb
レシピコントローラにハイライトされた行を追加します:
class Api::V1::RecipesController < ApplicationController
def index
recipe = Recipe.all.order(created_at: :desc)
render json: recipe
end
def create
end
def show
end
def destroy
end
end
`index`アクションでは、ActiveRecordの`all`メソッドを使用してデータベース内のすべてのレシピを取得します。`order`メソッドを使用して、作成日に基づいて降順で並べ替え、最新のレシピを最初に配置します。最後に、`render`を使用してレシピのリストをJSONレスポンスとして送信します。
次に、新しいレシピを作成するロジックを追加します。すべてのレシピを取得するのと同様に、提供されたレシピの詳細を検証し保存するためにActiveRecordに依存します。次のハイライトされたコード行でレシピコントローラを更新します:
class Api::V1::RecipesController < ApplicationController
def index
recipe = Recipe.all.order(created_at: :desc)
render json: recipe
end
def create
recipe = Recipe.create!(recipe_params)
if recipe
render json: recipe
else
render json: recipe.errors
end
end
def show
end
def destroy
end
private
def recipe_params
params.permit(:name, :image, :ingredients, :instruction)
end
end
createアクションでは、ActiveRecordのcreate
メソッドを使用して新しいレシピを作成します。create
メソッドは、コントローラーが提供されたすべてのパラメーターを一度にモデルに割り当てることができます。このメソッドを使用すると、レコードを簡単に作成できますが、悪意のある使用の可能性が開かれます。悪意のある使用は、Railsが提供するストロングパラメーター機能を使用して防ぐことができます。これにより、許可されていない限り、パラメーターを割り当てることができません。コードでcreate
メソッドにrecipe_params
パラメーターを渡します。recipe_params
は、コントローラーのパラメーターを許可してデータベースに誤ったまたは悪意のある内容が入らないようにするprivate
メソッドです。この場合、create
メソッドの正しい使用のためにname
、image
、ingredients
、およびinstruction
パラメーターを許可します。
レシピコントローラーは今、レシピの読み取りと作成ができます。残りの作業は、単一のレシピを読み取りおよび削除するためのロジックです。レシピコントローラーを以下のコードで更新してください:
class Api::V1::RecipesController < ApplicationController
before_action :set_recipe, only: %i[show destroy]
def index
recipe = Recipe.all.order(created_at: :desc)
render json: recipe
end
def create
recipe = Recipe.create!(recipe_params)
if recipe
render json: recipe
else
render json: recipe.errors
end
end
def show
render json: @recipe
end
def destroy
@recipe&.destroy
render json: { message: 'Recipe deleted!' }
end
private
def recipe_params
params.permit(:name, :image, :ingredients, :instruction)
end
def set_recipe
@recipe = Recipe.find(params[:id])
end
end
新しいコード行では、show
とdelete
アクションがリクエストと一致する場合にのみ、before_action
によって呼び出されるset_recipe
メソッドを作成します。set_recipe
メソッドでは、ActiveRecordのfind
メソッドを使用して、params
で提供されたid
と一致するレシピを検索し、それをインスタンス変数@recipe
に割り当てます。show
アクションでは、set_recipe
メソッドによって設定された@recipe
オブジェクトをJSONレスポンスとして返します。
destroy
アクションでは、Rubyの安全ナビゲーション演算子&.
を使用して、似たようなことを行いました。これにより、メソッドの呼び出し時にnil
エラーが発生しないようになります。この追加により、レシピが存在する場合にのみレシピを削除し、それに応答としてメッセージを送信できます。
recipes_controller.rb
へのこれらの変更を行った後、ファイルを保存して閉じます。
この手順では、レシピのためのモデルとコントローラを作成しました。バックエンドでレシピを操作するために必要なすべてのロジックを記述しました。次のセクションでは、レシピを表示するためのコンポーネントを作成します。
ステップ7 — レシピの表示
このセクションでは、レシピの表示用のコンポーネントを作成します。既存のすべてのレシピを表示するページと、個々のレシピを表示するページの2つを作成します。
最初に、すべてのレシピを表示するページを作成します。ページを作成する前に、データベースが現在空であるため、作業するためのレシピが必要です。Railsは、アプリケーションのためのシードデータを作成する方法を提供します。
編集するために、seeds.rb
というシードファイルを開きます:
- nano ~/rails_react_recipe/db/seeds.rb
シードファイルの初期コンテンツを以下のコードに置き換えます:
9.times do |i|
Recipe.create(
name: "Recipe #{i + 1}",
ingredients: '227g tub clotted cream, 25g butter, 1 tsp cornflour,100g parmesan, grated nutmeg, 250g fresh fettuccine or tagliatelle, snipped chives or chopped parsley to serve (optional)',
instruction: 'In a medium saucepan, stir the clotted cream, butter, and cornflour over a low-ish heat and bring to a low simmer. Turn off the heat and keep warm.'
)
end
このコードでは、Railsに9つのレシピを作成するように指示するループを使用しています。各レシピにはname
、ingredients
、instruction
のセクションがあります。ファイルを保存して終了します。
このデータでデータベースをシードするには、ターミナルで以下のコマンドを実行します:
- rails db:seed
このコマンドを実行すると、9つのレシピがデータベースに追加されます。これで、フロントエンドでそれらを取得して表示することができます。
すべてのレシピを表示するためのコンポーネントは、RecipesController
のindex
アクションにHTTPリクエストを行って、すべてのレシピのリストを取得します。これらのレシピは、その後、ページ上のカードに表示されます。
app/javascript/components
ディレクトリにRecipes.jsx
ファイルを作成します:
- nano ~/rails_react_recipe/app/javascript/components/Recipes.jsx
ファイルを開いたら、次の行を追加して、React
、useState
、useEffect
、Link
、useNavigate
モジュールをインポートします:
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
次に、ハイライトされた行を追加して、Recipes
という名前の機能的なReactコンポーネントを作成してエクスポートします:
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
const Recipes = () => {
const navigate = useNavigate();
const [recipes, setRecipes] = useState([]);
};
export default Recipes;
Recipe
コンポーネント内では、React RouterのナビゲーションAPIがuseNavigateフックを呼び出します。ReactのuseStateフックはrecipes
ステートを初期化し、これは空の配列([]
)であり、recipes
ステートを更新するsetRecipes
関数も初期化します。
次に、useEffect
フックで、すべてのレシピを取得するためにHTTPリクエストを行います。これを行うには、次の行を追加します:
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
const Recipes = () => {
const navigate = useNavigate();
const [recipes, setRecipes] = useState([]);
useEffect(() => {
const url = "/api/v1/recipes/index";
fetch(url)
.then((res) => {
if (res.ok) {
return res.json();
}
throw new Error("Network response was not ok.");
})
.then((res) => setRecipes(res))
.catch(() => navigate("/"));
}, []);
};
export default Recipes;
useEffect
フックでは、Fetch APIを使用してすべてのレシピを取得するHTTPコールを行います。レスポンスが成功した場合、アプリケーションはレシピの配列をrecipes
ステートに保存します。エラーが発生した場合、ユーザーをホームページにリダイレクトします。
最後に、コンポーネントがレンダリングされるときにブラウザーページに評価および表示される要素のマークアップを返します。この場合、コンポーネントはrecipes
ステートからレシピのカードをレンダリングします。次の行をRecipes.jsx
に追加します:
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
const Recipes = () => {
const navigate = useNavigate();
const [recipes, setRecipes] = useState([]);
useEffect(() => {
const url = "/api/v1/recipes/index";
fetch(url)
.then((res) => {
if (res.ok) {
return res.json();
}
throw new Error("Network response was not ok.");
})
.then((res) => setRecipes(res))
.catch(() => navigate("/"));
}, []);
const allRecipes = recipes.map((recipe, index) => (
<div key={index} className="col-md-6 col-lg-4">
<div className="card mb-4">
<img
src={recipe.image}
className="card-img-top"
alt={`${recipe.name} image`}
/>
<div className="card-body">
<h5 className="card-title">{recipe.name}</h5>
<Link to={`/recipe/${recipe.id}`} className="btn custom-button">
View Recipe
</Link>
</div>
</div>
</div>
));
const noRecipe = (
<div className="vw-100 vh-50 d-flex align-items-center justify-content-center">
<h4>
No recipes yet. Why not <Link to="/new_recipe">create one</Link>
</h4>
</div>
);
return (
<>
<section className="jumbotron jumbotron-fluid text-center">
<div className="container py-5">
<h1 className="display-4">Recipes for every occasion</h1>
<p className="lead text-muted">
We’ve pulled together our most popular recipes, our latest
additions, and our editor’s picks, so there’s sure to be something
tempting for you to try.
</p>
</div>
</section>
<div className="py-5">
<main className="container">
<div className="text-end mb-3">
<Link to="/recipe" className="btn custom-button">
Create New Recipe
</Link>
</div>
<div className="row">
{recipes.length > 0 ? allRecipes : noRecipe}
</div>
<Link to="/" className="btn btn-link">
Home
</Link>
</main>
</div>
</>
);
};
export default Recipes;
Recipes.jsx
を保存して終了します。
これで、すべてのレシピを表示するコンポーネントが作成されたので、それに対するルートを作成します。フロントエンドのルートファイルapp/javascript/routes/index.jsx
を開きます:
- nano app/javascript/routes/index.jsx
ファイルに次の行を追加します:
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";
export default (
<Router>
<Routes>
<Route path="/" exact component={Home} />
<Route path="/recipes" element={<Recipes />} />
</Routes>
</Router>
);
ファイルを保存して終了します。
この時点で、コードが期待通りに機能しているか確認するのは良いアイデアです。 以前と同様に、次のコマンドを使用してサーバーを起動します:
- bin/dev
次に、ブラウザーでアプリを開きます。 ホームページでView Recipeボタンを押して、シードレシピを表示するページにアクセスします:
ターミナルでCTRL+C
を使用してサーバーを停止し、プロンプトに戻ります。
今やアプリケーション内のすべてのレシピを表示できるようになったので、個々のレシピを表示するための2番目のコンポーネントを作成する時が来ました。 app/javascript/components
ディレクトリにRecipe.jsx
ファイルを作成します:
- nano app/javascript/components/Recipe.jsx
Recipes
コンポーネントと同様に、次の行を追加してReact
、useState
、useEffect
、Link
、useNavigate
、およびuseParam
モジュールをインポートします:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
次に、ハイライトされた行を追加して、Recipe
という名前の関数型Reactコンポーネントを作成してエクスポートします:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
};
export default Recipe;
Recipes
コンポーネントと同様に、React RouterナビゲーションをuseNavigate
フックで初期化します。 useState
フックでrecipe
ステートとsetRecipe
関数を初期化します。 さらに、URLパラメータのキー/値ペアを持つオブジェクトを返すuseParams
フックを呼び出します。
特定のレシピを見つけるには、アプリケーションがレシピのid
を知る必要があります。 これは、Recipe
コンポーネントがURLでid
param
を期待していることを意味します。 これには、useParams
フックの戻り値を保持するparams
オブジェクトを介してアクセスできます。
次に、useEffect
フックを宣言し、params
オブジェクトからid
param
にアクセスします。レシピのid
パラメーターを取得したら、レシピを取得するためにHTTPリクエストを行います。ファイルに以下の強調された行を追加してください:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
useEffect(() => {
const url = `/api/v1/show/${params.id}`;
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => setRecipe(response))
.catch(() => navigate("/recipes"));
}, [params.id]);
};
export default Recipe;
useEffect
フックでは、params.id
の値を使用して、id
を所有するレシピを取得するためにGET HTTPリクエストを行い、それをコンポーネントの状態にsetRecipe
関数を使用して保存します。レシピが存在しない場合、アプリはユーザーをレシピページにリダイレクトします。
次に、コンポーネント内で文字エンティティをHTMLエンティティに置き換えるaddHtmlEntities
関数を追加します。addHtmlEntities
関数は、文字列を受け取り、すべてのエスケープされた開き括弧と閉じ括弧をそれらのHTMLエンティティに置き換えます。この関数は、レシピの指示に保存されていたエスケープされた文字を変換するのに役立ちます。以下の強調された行を追加してください:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
useEffect(() => {
const url = `/api/v1/show/${params.id}`;
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => setRecipe(response))
.catch(() => navigate("/recipes"));
}, [params.id]);
const addHtmlEntities = (str) => {
return String(str).replace(/</g, "<").replace(/>/g, ">");
};
};
export default Recipe;
最後に、強調された行を追加して、コンポーネントの状態にあるレシピをページ上にレンダリングするためのマークアップを返します。
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
useEffect(() => {
const url = `/api/v1/show/${params.id}`;
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => setRecipe(response))
.catch(() => navigate("/recipes"));
}, [params.id]);
const addHtmlEntities = (str) => {
return String(str).replace(/</g, "<").replace(/>/g, ">");
};
const ingredientList = () => {
let ingredientList = "No ingredients available";
if (recipe.ingredients.length > 0) {
ingredientList = recipe.ingredients
.split(",")
.map((ingredient, index) => (
<li key={index} className="list-group-item">
{ingredient}
</li>
));
}
return ingredientList;
};
const recipeInstruction = addHtmlEntities(recipe.instruction);
return (
<div className="">
<div className="hero position-relative d-flex align-items-center justify-content-center">
<img
src={recipe.image}
alt={`${recipe.name} image`}
className="img-fluid position-absolute"
/>
<div className="overlay bg-dark position-absolute" />
<h1 className="display-4 position-relative text-white">
{recipe.name}
</h1>
</div>
<div className="container py-5">
<div className="row">
<div className="col-sm-12 col-lg-3">
<ul className="list-group">
<h5 className="mb-2">Ingredients</h5>
{ingredientList()}
</ul>
</div>
<div className="col-sm-12 col-lg-7">
<h5 className="mb-2">Preparation Instructions</h5>
<div
dangerouslySetInnerHTML={{
__html: `${recipeInstruction}`,
}}
/>
</div>
<div className="col-sm-12 col-lg-2">
<button
type="button"
className="btn btn-danger"
>
Delete Recipe
</button>
</div>
</div>
<Link to="/recipes" className="btn btn-link">
Back to recipes
</Link>
</div>
</div>
);
};
export default Recipe;
ingredientList
関数を使用すると、カンマで区切られたレシピの材料が配列に分割され、それをマップして材料のリストが作成されます。 材料がない場合、アプリは「利用可能な材料はありません」というメッセージを表示します。 レシピの指示に含まれるすべての開始および終了ブラケットも、addHtmlEntities
関数を通して置き換えます。 最後に、コードはレシピ画像をヒーロー画像として表示し、レシピ指示の隣にレシピの削除ボタンを追加し、レシピページへのリンクを追加します。
注意: ReactのdangerouslySetInnerHTML
属性を使用することはリスクが伴います。これにより、アプリがクロスサイトスクリプティング攻撃にさらされる可能性があります。 このリスクは、レシピを作成する際に入力された特殊文字が、NewRecipe
コンポーネントで宣言されたstripHtmlEntities
関数を使用して置換されることによって軽減されます。
保存してファイルを閉じます。
Recipe
コンポーネントをページで表示するには、それをルートファイルに追加します。 ルートファイルを編集するには、次の手順を実行します:
- nano app/javascript/routes/index.jsx
ファイルに以下のハイライトされた行を追加します:
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";
import Recipe from "../components/Recipe";
export default (
<Router>
<Routes>
<Route path="/" exact component={Home} />
<Route path="/recipes" exact component={Recipes} />
<Route path="/recipe/:id" element={<Recipe />} />
</Routes>
</Router>
);
このルートファイルでRecipe
コンポーネントをインポートし、ルートを追加します。 ルートには:id
param
があり、表示したいレシピのid
で置き換えられます。
ファイルを保存して閉じます。
bin/dev
スクリプトを使用してサーバーを再起動し、ブラウザでhttp://localhost:3000
にアクセスします。 View Recipesボタンをクリックしてレシピページに移動します。 レシピページでは、任意のレシピをクリックしてそのView Recipeボタンにアクセスします。 データベースからのデータでポップアップしたページが表示されます:
サーバーをCTRL+C
で停止できます。
このステップでは、データベースに9つのレシピを追加し、これらのレシピを個別およびコレクションとして表示するためのコンポーネントを作成しました。 次のステップでは、レシピを作成するコンポーネントを追加します。
ステップ8 — レシピの作成
使いやすい料理レシピアプリを作るための次のステップは、新しいレシピを作成する機能です。 このステップでは、この機能用のコンポーネントを作成します。 コンポーネントには、ユーザーから必要なレシピの詳細を収集するフォームが含まれ、その後、レシピデータを保存するためにRecipe
コントローラーのcreate
アクションにリクエストが行われます。
app/javascript/components
ディレクトリにNewRecipe.jsx
ファイルを作成します:
- nano app/javascript/components/NewRecipe.jsx
新しいファイルで、他のコンポーネントで使用したReact
、useState
、Link
、およびuseNavigate
モジュールをインポートします。
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
次に、ハイライトされた行を追加して、NewRecipe
コンポーネントを作成してエクスポートします。
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
const NewRecipe = () => {
const navigate = useNavigate();
const [name, setName] = useState("");
const [ingredients, setIngredients] = useState("");
const [instruction, setInstruction] = useState("");
};
export default NewRecipe;
前のコンポーネントと同様に、ReactルーターナビゲーションをuseNavigate
フックで初期化し、useState
フックを使用してname
、ingredients
、およびinstruction
の各状態を初期化します。それぞれに対応する更新関数があります。これらは有効なレシピを作成するために必要なフィールドです。
次に、stripHtmlEntities
関数を作成します。この関数は特殊文字(<
など)をそのエスケープ/エンコードされた値(<
など)に変換します。これには、NewRecipe
コンポーネントに以下のハイライトされた行を追加します:
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
const NewRecipe = () => {
const navigate = useNavigate();
const [name, setName] = useState("");
const [ingredients, setIngredients] = useState("");
const [instruction, setInstruction] = useState("");
const stripHtmlEntities = (str) => {
return String(str)
.replace(/\n/g, "<br> <br>")
.replace(/</g, "<")
.replace(/>/g, ">");
};
};
export default NewRecipe;
stripHtmlEntities
関数では、<
および>
文字をそれぞれエスケープされた値で置き換えます。これにより、データベースに生のHTMLを保存しなくて済みます。
次に、NewRecipe
コンポーネントにonChange
およびonSubmit
関数を追加して、フォームの編集と送信を処理します:
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
const NewRecipe = () => {
const navigate = useNavigate();
const [name, setName] = useState("");
const [ingredients, setIngredients] = useState("");
const [instruction, setInstruction] = useState("");
const stripHtmlEntities = (str) => {
return String(str)
.replace(/\n/g, "<br> <br>")
.replace(/</g, "<")
.replace(/>/g, ">");
};
const onChange = (event, setFunction) => {
setFunction(event.target.value);
};
const onSubmit = (event) => {
event.preventDefault();
const url = "/api/v1/recipes/create";
if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)
return;
const body = {
name,
ingredients,
instruction: stripHtmlEntities(instruction),
};
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch(url, {
method: "POST",
headers: {
"X-CSRF-Token": token,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
})
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => navigate(`/recipe/${response.id}`))
.catch((error) => console.log(error.message));
};
};
export default NewRecipe;
onChange
関数は、ユーザーの入力event
と状態設定関数を受け取り、その後ユーザーの入力値で状態を更新します。 onSubmit
関数では、必須の入力が空でないことを確認します。次に、新しいレシピを作成するために必要なパラメータを含むオブジェクトを構築します。 stripHtmlEntities
関数を使用して、レシピの指示内の<
および>
文字をそのエスケープされた値で置き換え、改行文字をブレークタグで置き換えることにより、ユーザーが入力したテキスト形式を保持します。 最後に、新しいレシピを作成するためのPOST HTTPリクエストを行い、成功した応答でそのページにリダイレクトします。
Cross-Site Request Forgery (CSRF)攻撃から保護するために、RailsはHTMLドキュメントにCSRFセキュリティトークンを添付します。このトークンは、GET
以外のリクエストが行われるたびに必要です。前のコードにあるtoken
定数を使用して、アプリケーションはサーバーでトークンを検証し、セキュリティトークンが期待されるものと一致しない場合は例外をスローします。 onSubmit
関数では、アプリケーションはRailsに埋め込まれたCSRFトークンを取得し、その後JSON文字列でHTTPリクエストを行います。レシピが正常に作成された場合、アプリケーションはユーザーをレシピページにリダイレクトし、新しく作成されたレシピを表示できます。
最後に、ユーザーが作成したいレシピの詳細を入力するためのフォームをレンダリングするマークアップを返します。ハイライトされた行を追加します:
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
const NewRecipe = () => {
const navigate = useNavigate();
const [name, setName] = useState("");
const [ingredients, setIngredients] = useState("");
const [instruction, setInstruction] = useState("");
const stripHtmlEntities = (str) => {
return String(str)
.replace(/\n/g, "<br> <br>")
.replace(/</g, "<")
.replace(/>/g, ">");
};
const onChange = (event, setFunction) => {
setFunction(event.target.value);
};
const onSubmit = (event) => {
event.preventDefault();
const url = "/api/v1/recipes/create";
if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)
return;
const body = {
name,
ingredients,
instruction: stripHtmlEntities(instruction),
};
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch(url, {
method: "POST",
headers: {
"X-CSRF-Token": token,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
})
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => navigate(`/recipe/${response.id}`))
.catch((error) => console.log(error.message));
};
return (
<div className="container mt-5">
<div className="row">
<div className="col-sm-12 col-lg-6 offset-lg-3">
<h1 className="font-weight-normal mb-5">
Add a new recipe to our awesome recipe collection.
</h1>
<form onSubmit={onSubmit}>
<div className="form-group">
<label htmlFor="recipeName">Recipe name</label>
<input
type="text"
name="name"
id="recipeName"
className="form-control"
required
onChange={(event) => onChange(event, setName)}
/>
</div>
<div className="form-group">
<label htmlFor="recipeIngredients">Ingredients</label>
<input
type="text"
name="ingredients"
id="recipeIngredients"
className="form-control"
required
onChange={(event) => onChange(event, setIngredients)}
/>
<small id="ingredientsHelp" className="form-text text-muted">
Separate each ingredient with a comma.
</small>
</div>
<label htmlFor="instruction">Preparation Instructions</label>
<textarea
className="form-control"
id="instruction"
name="instruction"
rows="5"
required
onChange={(event) => onChange(event, setInstruction)}
/>
<button type="submit" className="btn custom-button mt-3">
Create Recipe
</button>
<Link to="/recipes" className="btn btn-link mt-3">
Back to recipes
</Link>
</form>
</div>
</div>
</div>
);
};
export default NewRecipe;
返されたマークアップには、3つの入力フィールドが含まれるフォームが含まれています。それぞれが recipeName
、recipeIngredients
、instruction
の入力フィールドです。各入力フィールドには onChange
イベントハンドラがあり、onChange
関数を呼び出します。送信ボタンには onSubmit
イベントハンドラも付いており、フォームデータを送信する onSubmit
関数を呼び出します。
ファイルを保存して終了します。
このコンポーネントにブラウザでアクセスするには、ルートファイルをそのルートで更新します:
- nano app/javascript/routes/index.jsx
これらのハイライトされた行を含むルートファイルを更新します:
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";
import Recipe from "../components/Recipe";
import NewRecipe from "../components/NewRecipe";
export default (
<Router>
<Routes>
<Route path="/" exact component={Home} />
<Route path="/recipes" exact component={Recipes} />
<Route path="/recipe/:id" exact component={Recipe} />
<Route path="/recipe" element={<NewRecipe />} />
</Routes>
</Router>
);
ルートが設定されたら、ファイルを保存して閉じます。
開発サーバーを再起動し、ブラウザで http://localhost:3000
を開きます。レシピページに移動し、新しいレシピを作成 ボタンをクリックします。データベースにレシピを追加するフォームがあるページが表示されます:
必要なレシピの詳細を入力し、レシピを作成 ボタンをクリックします。新しく作成されたレシピがページに表示されます。準備ができたら、サーバーを閉じます。
このステップでは、料理レシピアプリケーションにレシピを作成する機能が追加されました。次のステップでは、レシピを削除する機能を追加します。
ステップ9 — レシピの削除
このセクションでは、レシピコンポーネントを変更して、レシピを削除するオプションを追加します。レシピページの削除ボタンをクリックすると、アプリケーションはデータベースからレシピを削除するリクエストを送信します。
まず、Recipe.jsx
ファイルを編集します:
- nano app/javascript/components/Recipe.jsx
Recipe
コンポーネントで、ハイライトされた行にdeleteRecipe
関数を追加します:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
useEffect(() => {
const url = `/api/v1/show/${params.id}`;
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => setRecipe(response))
.catch(() => navigate("/recipes"));
}, [params.id]);
const addHtmlEntities = (str) => {
return String(str).replace(/</g, "<").replace(/>/g, ">");
};
const deleteRecipe = () => {
const url = `/api/v1/destroy/${params.id}`;
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch(url, {
method: "DELETE",
headers: {
"X-CSRF-Token": token,
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then(() => navigate("/recipes"))
.catch((error) => console.log(error.message));
};
const ingredientList = () => {
let ingredientList = "No ingredients available";
if (recipe.ingredients.length > 0) {
ingredientList = recipe.ingredients
.split(",")
.map((ingredient, index) => (
<li key={index} className="list-group-item">
{ingredient}
</li>
));
}
return ingredientList;
};
const recipeInstruction = addHtmlEntities(recipe.instruction);
return (
<div className="">
...
deleteRecipe
関数では、削除するレシピのid
を取得し、その後URLを構築し、CSRFトークンを取得します。次に、Recipes
コントローラに対してレシピを削除するDELETE
リクエストを行います。レシピが正常に削除された場合、アプリケーションはユーザーをレシピページにリダイレクトします。
削除ボタンがクリックされるたびにdeleteRecipe
関数内のコードを実行するには、ボタンにクリックイベントハンドラとしてそれを渡します。コンポーネント内の削除ボタン要素にonClick
イベントを追加します:
...
return (
<div className="">
<div className="hero position-relative d-flex align-items-center justify-content-center">
<img
src={recipe.image}
alt={`${recipe.name} image`}
className="img-fluid position-absolute"
/>
<div className="overlay bg-dark position-absolute" />
<h1 className="display-4 position-relative text-white">
{recipe.name}
</h1>
</div>
<div className="container py-5">
<div className="row">
<div className="col-sm-12 col-lg-3">
<ul className="list-group">
<h5 className="mb-2">Ingredients</h5>
{ingredientList()}
</ul>
</div>
<div className="col-sm-12 col-lg-7">
<h5 className="mb-2">Preparation Instructions</h5>
<div
dangerouslySetInnerHTML={{
__html: `${recipeInstruction}`,
}}
/>
</div>
<div className="col-sm-12 col-lg-2">
<button
type="button"
className="btn btn-danger"
onClick={deleteRecipe}
>
Delete Recipe
</button>
</div>
</div>
<Link to="/recipes" className="btn btn-link">
Back to recipes
</Link>
</div>
</div>
);
...
このチュートリアルのこの時点で、完全なRecipe.jsx
ファイルはこのファイルと一致する必要があります:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
useEffect(() => {
const url = `/api/v1/show/${params.id}`;
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => setRecipe(response))
.catch(() => navigate("/recipes"));
}, [params.id]);
const addHtmlEntities = (str) => {
return String(str).replace(/</g, "<").replace(/>/g, ">");
};
const deleteRecipe = () => {
const url = `/api/v1/destroy/${params.id}`;
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch(url, {
method: "DELETE",
headers: {
"X-CSRF-Token": token,
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then(() => navigate("/recipes"))
.catch((error) => console.log(error.message));
};
const ingredientList = () => {
let ingredientList = "No ingredients available";
if (recipe.ingredients.length > 0) {
ingredientList = recipe.ingredients
.split(",")
.map((ingredient, index) => (
<li key={index} className="list-group-item">
{ingredient}
</li>
));
}
return ingredientList;
};
const recipeInstruction = addHtmlEntities(recipe.instruction);
return (
<div className="">
<div className="hero position-relative d-flex align-items-center justify-content-center">
<img
src={recipe.image}
alt={`${recipe.name} image`}
className="img-fluid position-absolute"
/>
<div className="overlay bg-dark position-absolute" />
<h1 className="display-4 position-relative text-white">
{recipe.name}
</h1>
</div>
<div className="container py-5">
<div className="row">
<div className="col-sm-12 col-lg-3">
<ul className="list-group">
<h5 className="mb-2">Ingredients</h5>
{ingredientList()}
</ul>
</div>
<div className="col-sm-12 col-lg-7">
<h5 className="mb-2">Preparation Instructions</h5>
<div
dangerouslySetInnerHTML={{
__html: `${recipeInstruction}`,
}}
/>
</div>
<div className="col-sm-12 col-lg-2">
<button
type="button"
className="btn btn-danger"
onClick={deleteRecipe}
>
Delete Recipe
</button>
</div>
</div>
<Link to="/recipes" className="btn btn-link">
Back to recipes
</Link>
</div>
</div>
);
};
export default Recipe;
ファイルを保存して終了します。
アプリケーションサーバーを再起動し、ホームページに移動します。すべての既存のレシピにアクセスするにはレシピを表示ボタンをクリックし、特定のレシピを開き、ページ上のレシピを削除ボタンをクリックして記事を削除します。レシピページにリダイレクトされ、削除されたレシピはもはや存在しません。
削除ボタンが機能するようになったので、完全に機能するレシピアプリケーションができました!
結論
このチュートリアルでは、Ruby on RailsとReactフロントエンドを使用して食事レシピアプリケーションを作成し、データベースとしてPostgreSQLを、スタイリングにはBootstrapを使用しました。Ruby on Railsでの開発を続けたい場合は、SSHトンネルを使用した3層Railsアプリケーションの通信のセキュリティ確保チュートリアルに従うか、Rubyのスキルをリフレッシュするために当社のRubyでコーディングする方法シリーズをご覧ください。Reactにさらに深く入りたい場合は、DigitalOcean APIからデータを表示する方法をお試しください。