CyStack logo
  • Sản phẩm & Dịch vụ
  • Giải pháp
  • Bảng giá
  • Công ty
  • Tài liệu
Vi

vi

Trang chủHướng dẫnCách thiết lập Ruby on Rails v7 với frontend React trên Ubuntu 20.04
Chuyên gia

Cách thiết lập Ruby on Rails v7 với frontend React trên Ubuntu 20.04

CyStack blog 37 phút để đọc
CyStack blog20/07/2025
Locker Avatar

Chris Pham

Technical Writer

Locker logo social
Reading Time: 37 minutes

Ruby on Rails là một framework ứng dụng web phía server rất phổ biến. Nó đứng sau nhiều ứng dụng web nổi tiếng hiện nay trên như GitHub, Basecamp, SoundCloud, Airbnb và Twitch. Với việc tập trung vào trải nghiệm của lập trình viên và một cộng đồng nhiệt huyết, Ruby on Rails cung cấp cho bạn các công cụ cần thiết để xây dựng và duy trì các ứng dụng web hiện đại.

thiết lập Ruby on Rails v7

React là một thư viện JavaScript dùng để xây dựng giao diện người dùng (UI) phía frontend. Được hậu thuẫn bởi Facebook, đây là một trong những thư viện frontend phổ biến nhất hiện nay trên web. React cung cấp các tính năng như Virtual DOM (Document Object Model ảo), kiến trúc thành phần (component architecture) và quản lý trạng thái (state management), giúp quá trình phát triển frontend trở nên có tổ chức và hiệu quả hơn.

Khi xu hướng phát triển frontend hiện nay đang chuyển dịch sang các framework tách biệt khỏi mã nguồn phía server, việc kết hợp sự tinh gọn của Rails với hiệu năng của React sẽ giúp bạn xây dựng những ứng dụng hiện đại và mạnh mẽ, bắt kịp xu thế. Bằng cách sử dụng React để render các component từ trong một view của Rails (thay vì dùng template engine mặc định của Rails), ứng dụng của bạn sẽ được hưởng lợi từ những cải tiến mới nhất trong JavaScript và phát triển frontend, đồng thời vẫn tận dụng được tính biểu cảm mạnh mẽ của Ruby on Rails.

Trong hướng dẫn này, bạn sẽ tạo một ứng dụng Ruby on Rails để lưu trữ các công thức nấu ăn yêu thích, sau đó hiển thị chúng bằng giao diện frontend React. Sau khi hoàn thành, bạn sẽ có thể tạo, xem và xóa công thức nấu ăn thông qua một giao diện React được thiết kế với Bootstrap.

thiết lập Ruby on Rails v7

Yêu cầu trước khi bắt đầu

Để làm theo hướng dẫn này, bạn cần:

  • Node.js và npm đã được cài đặt trên máy tính dùng để phát triển. Hướng dẫn này sử dụng Node.js phiên bản 16.14.0 và npm phiên bản 8.3.1.Node.js là một môi trường chạy JavaScript cho phép bạn thực thi mã ngoài trình duyệt. Nó đi kèm với trình quản lý gói (Package Manager) được cài sẵn gọi là npm, giúp bạn dễ dàng cài đặt và cập nhật các gói phần mềm.
  • Trình quản lý gói Yarn đã được cài đặt trên máy phát triển. Công cụ này cho phép bạn tải framework React. Hướng dẫn này được kiểm thử với Yarn phiên bản 1.22.10; để cài đặt gói phụ thuộc này, bạn hãy làm theo hướng dẫn cài đặt chính thức của Yarn.
  • Ruby on Rails đã được cài đặt. Hướng dẫn này sử dụng Ruby phiên bản 3.1.2 và Rails phiên bản 7.0.4, vì vậy hãy đảm bảo bạn chỉ định đúng các phiên bản này trong quá trình cài đặt.

Lưu ý: Rails phiên bản 7 không tương thích ngược (not backward-compatible). Nếu bạn đang sử dụng Rails phiên bản 5, vui lòng tham khảo bài hướng dẫn để thiết lập dự án Ruby on Rails v5 với frontend React trên Ubuntu 18.04.

Bạn cũng cần cài đặt PostgreSQL, như được mô tả trong Bước 1 và 2 của bài hướng dẫn sử dụng PostgreSQL với ứng dụng ruby on Rails trên Ubuntu 18.04. Để làm theo hướng dẫn này, bạn có thể sử dụng PostgreSQL phiên bản 12 trở lên. Nếu bạn muốn phát triển ứng dụng này trên một bản phân phối Linux khác hoặc một hệ điều hành khác, hãy tham khảo trang tải xuống chính thức của PostgreSQL.

Cách thiết lập Ruby on Rails v7

Bước 1 – Tạo một ứng dụng Rails mới

Trong bước này, bạn sẽ xây dựng ứng dụng công thức nấu ăn của mình dựa trên framework Rails. Trước tiên, bạn sẽ tạo một ứng dụng Rails mới và thiết lập nó để hoạt động cùng với React.

Rails cung cấp một số script có tên là generators, giúp bạn tự động tạo ra tất cả những gì cần thiết để xây dựng một ứng dụng web hiện đại.

Để xem danh sách đầy đủ các lệnh này và chức năng của từng cái, hãy chạy lệnh sau trong terminal:

$ rails -h

Lệnh này sẽ hiển thị một danh sách đầy đủ các tùy chọn, cho phép bạn thiết lập các thông số cho ứng dụng của mình. Một trong các lệnh được liệt kê là lệnh new, dùng để tạo một ứng dụng Rails mới.

Bây giờ, bạn sẽ tạo một ứng dụng Rails mới bằng cách sử dụng generator new. Hãy chạy lệnh sau trong terminal của bạn:

$ rails new rails_react_recipe -d postgresql -j esbuild -c bootstrap -T

Lệnh trước đó sẽ tạo một ứng dụng Rails mới trong thư mục có tên là rails_react_recipe, cài đặt các phụ thuộc Ruby và JavaScript cần thiết, đồng thời cấu hình Webpack. Các tùy chọn (flags) đi kèm với lệnh generator new bao gồm:

  • -d: Chỉ định hệ quản trị cơ sở dữ liệu (database engine) ưu tiên. Trong trường hợp này là PostgreSQL.
  • -j: Chỉ định cách xử lý JavaScript trong ứng dụng. Rails cung cấp một vài tùy chọn để xử lý mã JavaScript. Giá trị esbuild được truyền vào -j để Rails tự động cấu hình esbuild làm công cụ bundler (đóng gói) JavaScript.
  • -c: Chỉ định trình xử lý CSS cho ứng dụng. Trong hướng dẫn này, lựa chọn ưu tiên là Bootstrap.
  • -T: Yêu cầu Rails bỏ qua việc tạo các file kiểm thử (test files), vì bạn sẽ không viết test trong hướng dẫn này. Tùy chọn này cũng hữu ích nếu bạn muốn sử dụng công cụ kiểm thử Ruby khác thay vì công cụ mặc định của Rails.

Sau khi lệnh chạy xong, hãy chuyển vào thư mục rails_react_recipe, đây là thư mục gốc của ứng dụng:

$ cd rails_react_recipe

Tiếp theo, liệt kê các nội dung trong thư mục bằng lệnh:

$ ls

Các nội dung hiển thị sẽ tương tự như sau:

Output
Gemfile       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

Thư mục gốc này chứa nhiều file và thư mục được tự động tạo, tạo thành cấu trúc cơ bản của một ứng dụng Rails. Trong đó có cả tệp package.json chứa các phụ thuộc cần thiết cho ứng dụng React.

Bây giờ, bạn đã tạo thành công một ứng dụng Rails mới. Ở bước tiếp theo, bạn sẽ kết nối ứng dụng với cơ sở dữ liệu (database).

Bước 2 – Thiết lập cơ sở dữ liệu

Trước khi chạy ứng dụng Rails mới, bạn cần kết nối ứng dụng với một cơ sở dữ liệu. Trong bước này, bạn sẽ kết nối ứng dụng Rails vừa tạo với cơ sở dữ liệu PostgreSQL, để có thể lưu trữ và truy xuất dữ liệu về công thức nấu ăn khi cần thiết.

Tệp cấu hình database.yml, nằm tại config/database.yml, chứa các thông tin chi tiết về cơ sở dữ liệu như tên database cho các môi trường phát triển khác nhau. Rails đặt tên cơ sở dữ liệu cho từng môi trường bằng cách thêm dấu gạch dưới (_) và tên môi trường vào tên gốc. Trong hướng dẫn này, bạn sẽ sử dụng các giá trị cấu hình mặc định, nhưng bạn hoàn toàn có thể thay đổi chúng nếu cần thiết.

Lưu ý: Ở thời điểm này, bạn có thể chỉnh sửa tệp config/database.yml để thiết lập PostgreSQL role (vai trò/người dùng PostgreSQL) mà bạn muốn Rails sử dụng để tạo cơ sở dữ liệu. Trong phần chuẩn bị ban đầu, bạn đã tạo một role được bảo vệ bằng mật khẩu. Nếu bạn chưa thiết lập người dùng PostgreSQL, bạn có thể thực hiện ngay bây giờ .

Rails cung cấp nhiều lệnh giúp việc phát triển ứng dụng web trở nên đơn giản hơn, bao gồm cả các lệnh làm việc với cơ sở dữ liệu như create, dropreset. Để tạo cơ sở dữ liệu cho ứng dụng của bạn, hãy chạy lệnh sau trong terminal:

$ rails db:create

Lệnh này sẽ tạo cơ sở dữ liệu cho hai môi trường: developmenttest, và hiển thị kết quả đầu ra tương tự như sau:

Output
Created database 'rails_react_recipe_development'
Created database 'rails_react_recipe_test'

Bây giờ ứng dụng đã được kết nối với cơ sở dữ liệu, bạn có thể khởi chạy nó bằng lệnh sau:

$ bin/dev

Rails cung cấp một script thay thế có tên bin/dev, dùng để khởi động ứng dụng Rails bằng cách thực thi các lệnh trong tệp Procfile.dev (nằm ở thư mục gốc của ứng dụng) thông qua gem Foreman.

Sau khi bạn chạy lệnh này, dòng lệnh (command prompt) ****sẽ tạm thời biến mất và thay vào đó là một loạt thông báo đầu ra như sau:

Output
started 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.

Để truy cập vào ứng dụng của bạn, hãy mở một cửa sổ trình duyệt và truy cập vào địa chỉ http://localhost:3000. Trang chào mừng mặc định của Rails sẽ được tải lên, điều đó có nghĩa là bạn đã thiết lập ứng dụng Rails thành công.

cài đặt Ruby on Rails v7

Để dừng máy chủ web, hãy nhấn CTRL+C trong terminal nơi server đang chạy. Bạn sẽ nhận được một thông báo tạm biệt từ Puma như sau:

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

Dấu nhắc lệnh trong terminal của bạn sẽ xuất hiện trở lại.

Bạn đã thiết lập thành công cơ sở dữ liệu cho ứng dụng công thức nấu ăn của mình. Ở bước tiếp theo, bạn sẽ tiến hành cài đặt các thư viện JavaScript cần thiết để xây dựng phần giao diện React (React frontend) cho ứng dụng.

Bước 3 – Cài đặt các thư viện phụ thuộc cho giao diện (Frontend Dependencies)

Trong bước này, bạn sẽ cài đặt các thư viện JavaScript cần thiết cho phần giao diện (frontend) của ứng dụng công thức nấu ăn của bạn. Chúng bao gồm:

  • React – dùng để xây dựng giao diện người dùng.
  • React DOM – giúp React tương tác với DOM của trình duyệt.
  • React Router – dùng để xử lý điều hướng (navigation) trong ứng dụng React.

Chạy lệnh sau bằng trình quản lý gói Yarn để cài đặt các thư viện này:


$ yarn add react react-dom react-router-dom

Lệnh này sử dụng Yarn để cài đặt các gói đã chỉ định và thêm chúng vào tệp package.json. Để kiểm tra, hãy mở tệp package.json nằm trong thư mục gốc của dự án:

$ nano package.json

Các gói đã được cài đặt sẽ xuất hiện dưới khóa dependencies

~/rails_react_recipe/package.json
{
  "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"
  }
}

Đóng tệp lại bằng cách nhấn CTRL+X.

Bạn đã cài đặt xong một số thư viện phụ trợ cho phần giao diện (frontend) của ứng dụng. Tiếp theo, bạn sẽ thiết lập trang chủ cho ứng dụng công thức nấu ăn của mình.

Bước 4 – Thiết lập trang chủ (Homepage)

Sau khi đã cài đặt đầy đủ các dependency cần thiết, bạn sẽ tiến hành tạo một trang chủ cho ứng dụng, trang này sẽ đóng vai trò là trang đích (landing page) khi người dùng truy cập vào ứng dụng lần đầu.

Rails tuân theo mô hình kiến trúc MVC (Model–View–Controller) trong việc xây dựng ứng dụng. Trong mô hình này, Controller (bộ điều khiển) có nhiệm vụ nhận các yêu cầu cụ thể và chuyển tiếp chúng đến Model (mô hình) hoặc View (giao diện) phù hợp. Hiện tại, ứng dụng đang hiển thị trang chào mừng mặc định của Rails khi bạn truy cập địa chỉ gốc (root URL) trên trình duyệt. Để thay đổi điều đó, bạn sẽ tạo một controller và view cho trang chủ và gán (match) nó với một route thích hợp.

Rails cung cấp một công cụ dòng lệnh gọi là controller generator để tạo controller một cách tự động. Generator này nhận vào tên controller và tên action tương ứng. Bạn có thể tìm hiểu thêm trong tài liệu chính thức của Rails.

Trong hướng dẫn này, controller sẽ được đặt tên là Homepage. Chạy lệnh sau để tạo controller Homepage với action index:

$ rails g controller Homepage index

Lưu ý: Trên hệ điều hành Linux, lỗi FATAL: Listen error: unable to monitor directories for changes.có thể xảy ra do hệ thống giới hạn số lượng tệp mà máy có thể theo dõi để phát hiện thay đổi. Hãy chạy lệnh sau để khắc phục lỗi:

$ echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p

Lệnh này sẽ tăng vĩnh viễn số lượng thư mục mà Listen có thể theo dõi lên đến 524288.

Bạn có thể thay đổi giá trị này bất cứ lúc nào bằng cách chạy lại cùng lệnh và thay thế 524288 bằng con số bạn mong muốn.

Việc chạy lệnh tạo controller sẽ sinh ra các tệp sau:

  • Một tệp homepage_controller.rb để xử lý tất cả các yêu cầu liên quan đến trang chủ (homepage). Tệp này chứa action index mà bạn đã chỉ định trong lệnh.
  • Một tệp homepage_helper.rb dùng để thêm các phương thức hỗ trợ (helper methods) liên quan đến controller Homepage.
  • Một tệp index.html.erb đóng vai trò là trang giao diện (view) để hiển thị bất kỳ nội dung nào liên quan đến trang chủ.

Ngoài các trang mới được tạo ra khi bạn chạy lệnh Rails, Rails còn cập nhật tệp định tuyến nằm tại config/routes.rb bằng cách thêm một route get cho trang chủ. Bạn sẽ chỉnh sửa route này để đặt nó làm root route (tuyến mặc định của ứng dụng).

Root route trong Rails dùng để xác định nội dung nào sẽ được hiển thị khi người dùng truy cập vào địa chỉ gốc (root URL) của ứng dụng. Trong trường hợp này, bạn muốn người dùng nhìn thấy trang chủ của mình. Hãy mở tệp config/routes.rb bằng trình soạn thảo mà bạn yêu thích để tiến hành chỉnh sửa:

$ nano config/routes.rb

Trong tệp này, hãy thay dòng get 'homepage/index' bằng root 'homepage#index' để nội dung tệp giống như sau:

~/rails_react_recipe/config/routes.rb
Rails.application.routes.draw do
  root 'homepage#index'
  # For details on the DSL available within this file, see <http://guides.rubyonrails.org/routing.html>
end

Thay đổi này sẽ hướng dẫn Rails ánh xạ các yêu cầu đến địa chỉ gốc của ứng dụng (root URL) tới action index trong controller Homepage. Action này sẽ hiển thị trên trình duyệt nội dung được viết trong tệp index.html.erb, nằm tại đường dẫn app/views/homepage/index.html.erb.

Lưu và đóng tệp lại.

Để kiểm tra xem thay đổi đã hoạt động hay chưa, hãy khởi động ứng dụng của bạn bằng lệnh:

$ bin/dev

Khi bạn mở hoặc làm mới ứng dụng trên trình duyệt, một trang đích (landing page) mới của ứng dụng sẽ được tải lên.

Sau khi bạn đã kiểm tra và xác nhận rằng ứng dụng đang hoạt động, hãy nhấn CTRL+C để dừng máy chủ (server).

Tiếp theo, mở tệp ~/rails_react_recipe/app/views/homepage/index.html.erb:

$ nano ~/rails_react_recipe/app/views/homepage/index.html.erb

Xoá toàn bộ mã bên trong tệp, sau đó lưu lại tệp ở trạng thái trống. Việc này đảm bảo rằng nội dung trong index.html.erb sẽ không gây ảnh hưởng đến quá trình hiển thị giao diện frontend bằng React.

Giờ đây, sau khi đã thiết lập xong trang chủ cho ứng dụng, bạn có thể chuyển sang phần tiếp theo – nơi bạn sẽ cấu hình phần giao diện frontend của ứng dụng để sử dụng React.

Bước 5 – Cấu hình React làm giao diện (Frontend) cho ứng dụng Rails của bạn

Ở bước này, bạn sẽ cấu hình Rails để sử dụng React cho phần frontend của ứng dụng, thay vì sử dụng công cụ template mặc định của nó. Cấu hình mới này sẽ cho phép bạn tạo một trang chủ hấp dẫn hơn về mặt giao diện bằng React.

Với tùy chọn esbuild đã được chỉ định khi khởi tạo ứng dụng Rails, phần lớn các thiết lập cần thiết để JavaScript hoạt động trơn tru với Rails đã được thiết lập sẵn. Việc còn lại chỉ là nạp entry point của ứng dụng React vào entry point của esbuild dành cho các file JavaScript. Để làm điều đó, hãy bắt đầu bằng cách tạo thư mục components bên trong thư mục app/javascript:

$ mkdir ~/rails_react_recipe/app/javascript/components

Thư mục components sẽ chứa component dành cho trang chủ, cùng với các React component khác trong ứng dụng, bao gồm cả file entry để khởi động ứng dụng React.

Tiếp theo, mở file application.js nằm trong app/javascript/application.js:

$ nano ~/rails_react_recipe/app/javascript/application.js

Thêm dòng mã được đánh dấu vào file:

~/rails_react_recipe/app/javascript/application.js
// Entry point for the build script in your package.json
import "@hotwired/turbo-rails"
import "./controllers"
import * as bootstrap from "bootstrap"
import "./components"

Dòng mã được thêm vào file application.js sẽ import mã trong mục file index.jsx, giúp esbuild nhận diện và đóng gói (bundle) mã React vào quá trình build. Khi thư mục /components đã được import vào mục point JavaScript của ứng dụng Rails, bạn có thể bắt đầu tạo một React component cho trang chủ. Trang chủ này sẽ chứa một vài đoạn văn bản và một nút kêu gọi hành động (call to action) để xem tất cả các công thức nấu ăn.

Lưu và đóng file lại.

Sau đó, tạo một file Home.jsx trong thư mục components:

$ nano ~/rails_react_recipe/app/javascript/components/Home.jsx

Thêm đoạn mã sau vào file:

~/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>
);

Trong đoạn mã này, bạn nhập React và component Link từ React Router. Component Link được dùng để tạo một siêu liên kết, cho phép điều hướng từ trang này sang trang khác trong ứng dụng. Tiếp theo, bạn tạo và import một functional component (component dạng hàm) chứa một số markup (ngôn ngữ đánh dấu) cho trang chủ, được tạo kiểu bằng các lớp của Bootstrap.

Lưu và đóng file lại.

Với component Home đã sẵn sàng, bạn sẽ tiến hành thiết lập routing bằng React Router. Hãy tạo một thư mục routes trong thư mục app/javascript:

$ mkdir ~/rails_react_recipe/app/javascript/routes

Thêm đoạn mã sau vào đó:

~/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>
);

Trong file index.jsx này, bạn import các module sau: module React để có thể sử dụng React, cùng với các module BrowserRouter, Routes và Route từ React Router, những module này phối hợp với nhau giúp bạn điều hướng giữa các tuyến đường trong ứng dụng. Cuối cùng, bạn import component Home, component này sẽ được render bất cứ khi nào có yêu cầu khớp với route gốc (/). Khi bạn muốn thêm các trang mới vào ứng dụng, bạn chỉ cần khai báo thêm các route trong file này và liên kết chúng với component tương ứng mà bạn muốn hiển thị cho mỗi trang.

Lưu và thoát file.

Bây giờ, bạn đã thiết lập định tuyến (routing) bằng React Router. Để React nhận biết và sử dụng các route này, bạn cần đảm bảo các route được nạp tại entry point của ứng dụng.

Để làm điều đó, bạn sẽ render các route bên trong một component, và component này sẽ được React sử dụng trong file entry.

Hãy tạo một file App.jsx trong thư mục app/javascript/components:

$ nano ~/rails_react_recipe/app/javascript/components/App.jsx

Thêm đoạn mã sau vào file App.jsx:

~/rails_react_recipe/app/javascript/components/App.jsx
import React from "react";
import Routes from "../routes";

export default props => <>{Routes}</>;

Trong file App.jsx, bạn import React và file định tuyến (routes) mà bạn vừa tạo. Sau đó, bạn export một component để render các route bên trong React Fragment. Component này sẽ được render tại entry point của ứng dụng, giúp các route khả dụng mỗi khi ứng dụng được tải.

Lưu và đóng file lại.

Bây giờ, khi bạn đã thiết lập xong App.jsx, bạn có thể render nó trong file entry của ứng dụng. Hãy tạo một file index.jsx trong thư mục components:

$ nano ~/rails_react_recipe/app/javascript/components/index.jsx

Thêm đoạn mã sau vào file index.jsx:

~/rails_react_recipe/app/javascript/components/index.jsx
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 />);
});

Trong các dòng import, bạn import thư viện React, hàm createRoot từ ReactDOM, và component App của bạn. Bằng cách sử dụng hàm createRoot của ReactDOM, bạn tạo một root element dưới dạng một thẻ div được gắn vào trang web, và render component App bên trong thẻ div đó. Khi ứng dụng được tải, React sẽ render nội dung của component App vào bên trong phần tử div trên trang.

Lưu và đóng file lại.

Cuối cùng, bạn sẽ thêm một số CSS styles cho trang chủ của mình. Mở file application.bootstrap.scss nằm trong thư mục~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss

$ nano ~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss

Tiếp theo, thay thế toàn bộ nội dung của file application.bootstrap.scss bằng đoạn mã sau:

~/rails_react_recipe/app/assets/stylesheets/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;
}

Bạn đã thiết lập một số màu sắc tùy chỉnh cho trang web. Phần .hero sẽ tạo cấu trúc cho hero image – một hình ảnh lớn hoặc banner nổi bật ở trang chủ của website, mà bạn sẽ thêm vào sau. Ngoài ra, class .custom-button.btn sẽ định dạng nút bấm mà người dùng sẽ sử dụng để truy cập vào ứng dụng.

Khi các kiểu CSS đã được thiết lập xong, lưu và thoát file.

Tiếp theo, khởi động lại web server cho ứng dụng của bạn:

$ bin/dev

Sau đó, hãy tải lại ứng dụng trong trình duyệt của bạn. Một trang chủ hoàn toàn mới sẽ được tải lên:

cách thiết lập Ruby on Rails v7

Dừng máy chủ web bằng tổ hợp phím CTRL+C.

Ở bước này, bạn đã cấu hình ứng dụng để sử dụng React làm giao diện người dùng. Trong bước tiếp theo, bạn sẽ tạo các modelvà bộ điều khiển (controller cho phép bạn tạo, đọc, cập nhật và xóa các công thức làm món ăn.

Bước 6 – Tạo Controller và Model cho Recipe:

Bây giờ bạn đã thiết lập giao diện frontend React cho ứng dụng của mình, bạn sẽ tạo một model và controller cho Recipe. Model Recipe sẽ đại diện cho bảng cơ sở dữ liệu chứa thông tin về các công thức nấu ăn của người dùng, trong khi controller sẽ tiếp nhận và xử lý các yêu cầu tạo, đọc, cập nhật hoặc xóa công thức. Khi người dùng yêu cầu một công thức, controller Recipe sẽ nhận yêu cầu đó và chuyển đến model Recipe, model này sẽ truy xuất dữ liệu được yêu cầu từ cơ sở dữ liệu. Sau đó, model trả dữ liệu công thức như một phản hồi cho controller. Cuối cùng, thông tin này được hiển thị trong trình duyệt.

Bắt đầu bằng cách tạo model Recipe bằng cách sử dụng lệnh con generate model do Rails cung cấp, chỉ định tên model cùng với các cột và kiểu dữ liệu của nó. Chạy lệnh sau:

$ rails generate model Recipe name:string ingredients:text instruction:text image:string

Lệnh trên yêu cầu Rails tạo một model Recipe cùng với một cột name có kiểu dữ liệu string, các cột ingredients và instruction có kiểu dữ liệu text, và một cột image có kiểu dữ liệu string. Hướng dẫn này đặt tên model là Recipe vì trong Rails, các model sử dụng tên số ít trong khi các bảng tương ứng trong cơ sở dữ liệu sử dụng tên số nhiều.

Việc chạy lệnh generate model sẽ tạo ra hai tệp và in ra nội dung sau:

Output
      invoke  active_record
      create    db/migrate/20221017220817_create_recipes.rb
      create    app/models/recipe.rb

Hai tệp được tạo ra là:

Một tệp recipe.rb chứa toàn bộ logic liên quan đến model.

Một tệp 20221017220817_create_recipes.rb (con số ở đầu tên tệp có thể khác tùy vào ngày bạn chạy lệnh). Tệp migration này chứa các chỉ dẫn để tạo cấu trúc cơ sở dữ liệu.

Tiếp theo, bạn sẽ chỉnh sửa tệp model recipe để đảm bảo rằng chỉ dữ liệu hợp lệ mới được lưu vào cơ sở dữ liệu. Bạn có thể thực hiện điều này bằng cách thêm một số xác thực (validation) vào model.

Mở tệp model recipe nằm tại app/models/recipe.rb:

$ nano ~/rails_react_recipe/app/models/recipe.rb

Thêm các dòng mã được đánh dấu sau vào tệp:

~/rails_react_recipe/app/models/recipe.rb
class Recipe < ApplicationRecord
  validates :name, presence: true
  validates :ingredients, presence: true
  validates :instruction, presence: true
end

Trong đoạn mã này, bạn thêm phần xác thực (validation) cho model, nhằm kiểm tra sự hiện diện của các trường name, ingredientsinstruction. Nếu thiếu một trong ba trường này, công thức sẽ không hợp lệ và sẽ không được lưu vào cơ sở dữ liệu.

Lưu và đóng tệp.

Để Rails tạo bảng recipes trong cơ sở dữ liệu của bạn, bạn cần chạy migration**,** một cách thay đổi cấu trúc cơ sở dữ liệu bằng lập trình.

Để đảm bảo migration hoạt động với cơ sở dữ liệu mà bạn đã thiết lập, bạn cần chỉnh sửa tệp 20221017220817_create_recipes.rb.

Mở tệp này trong trình soạn thảo của bạn:

$ nano ~/rails_react_recipe/db/migrate/20221017220817_create_recipes.rb

Thêm các phần được đánh dấu để tệp của bạn giống như sau:

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

Tệp migration này chứa một lớp Ruby với phương thức change và một lệnh để tạo bảng có tên là recipes cùng với các cột và kiểu dữ liệu tương ứng. Bạn cũng đã cập nhật tệp 20221017220817_create_recipes.rb bằng cách thêm ràng buộc NOT NULL cho các cột name, ingredientsinstruction bằng cách thêm null: false, đảm bảo rằng các cột này phải có giá trị trước khi thay đổi cơ sở dữ liệu. Cuối cùng, bạn thêm một URL hình ảnh mặc định cho cột image; bạn có thể thay bằng URL khác nếu muốn sử dụng hình ảnh khác.

Với những thay đổi này, hãy lưu và thoát khỏi tệp. Bây giờ bạn đã sẵn sàng chạy migration và tạo bảng của mình. Trong terminal, hãy chạy lệnh sau:

$ rails db:migrate

Bạn sử dụng lệnh database migrate để thực thi các chỉ dẫn trong tệp migration của mình. Khi lệnh chạy thành công, bạn sẽ nhận được một kết quả đầu ra tương tự như sau:

Output
== 20190407161357 CreateRecipes: migrating ====================================
-- create_table(:recipes)
   -> 0.0140s
== 20190407161357 CreateRecipes: migrated (0.0141s) ===========================

Sau khi đã tạo xong model recipe, bước tiếp theo bạn sẽ tạo controller cho recipes để thêm logic xử lý việc tạo, đọc và xóa các công thức. Hãy chạy lệnh sau:

$ rails generate controller api/v1/Recipes index create show destroy  --skip-template-engine --no-helper

Trong lệnh này, bạn tạo một controller Recipes trong thư mục api/v1 với các action index, create, showdestroy. Action index sẽ xử lý việc lấy tất cả các công thức; Action create sẽ chịu trách nhiệm tạo công thức mới; Action show sẽ lấy một công thức cụ thể; và action destroy sẽ chứa logic để xóa một công thức.

Bạn cũng truyền vào một số cờ (flags) để làm cho controller nhẹ hơn, bao gồm:

  • -skip-template-engine, yêu cầu Rails bỏ qua việc tạo các tệp view vì React sẽ đảm nhận phần giao diện.
  • -no-helper, yêu cầu Rails bỏ qua việc tạo tệp helper cho controller của bạn.

Việc chạy lệnh cũng sẽ cập nhật tệp routes với một tuyến (route) cho mỗi action trong controller Recipes.

Khi lệnh được chạy, nó sẽ in ra một kết quả như sau:

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

Để sử dụng các tuyến (routes) này, bạn sẽ cần chỉnh sửa tệp config/routes.rb. Hãy mở tệp routes.rb trong trình soạn thảo văn bản của bạn:

$ nano ~/rails_react_recipe/config/routes.rb

Cập nhật tệp này để giống với đoạn mã dưới đây, thay đổi hoặc thêm các dòng được đánh dấu:

~/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'
  # Define your application routes per the DSL in <https://guides.rubyonrails.org/routing.html>

  # Defines the root path route ("/")
  # root "articles#index"
end

Trong tệp routes này, bạn sửa đổi HTTP verb của các tuyến createdestroy để cho phép gửi dữ liệu thông qua phương thức post và xóa dữ liệu thông qua phương thức delete.

Bạn cũng chỉnh sửa các tuyến cho action showdestroy bằng cách thêm tham số :id vào đường dẫn. :id sẽ chứa mã định danh của công thức bạn muốn đọc hoặc xóa.

Bạn thêm một tuyến bắt tất cả với get '/*path', tuyến này sẽ chuyển bất kỳ yêu cầu nào không khớp với các tuyến đã định nghĩa đến action index của controller homepage. Việc định tuyến phía front-end sẽ xử lý các yêu cầu không liên quan đến việc tạo, đọc hoặc xóa công thức.

Lưu và thoát khỏi tệp.

Để kiểm tra danh sách các route hiện có trong ứng dụng của bạn, hãy chạy lệnh sau:

$ rails routes

Việc chạy lệnh này sẽ hiển thị một danh sách dài gồm các mẫu URI, phương thức HTTP (verb), và các controller hoặc action tương ứng trong dự án của bạn.

Tiếp theo, bạn sẽ thêm logic để lấy tất cả các công thức cùng một lúc. Rails sử dụng thư viện ActiveRecord để xử lý các tác vụ liên quan đến cơ sở dữ liệu như thế này. ActiveRecord liên kết các lớp Ruby với các bảng trong cơ sở dữ liệu quan hệ và cung cấp một API mạnh mẽ để làm việc với chúng.

Để lấy tất cả công thức, bạn sẽ sử dụng ActiveRecord để truy vấn bảng recipes và lấy toàn bộ dữ liệu trong cơ sở dữ liệu.

Hãy mở tệp recipes_controller.rb bằng lệnh sau:

$ nano ~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb

Hãy thêm các dòng mã được đánh dấu sau vào controller:

~/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

Trong action index, bạn sử dụng phương thức all của ActiveRecord để lấy tất cả các công thức có trong cơ sở dữ liệu. Bằng cách sử dụng phương thức order, bạn sắp xếp chúng theo thứ tự giảm dần dựa trên ngày tạo, điều này sẽ đặt các công thức mới nhất lên đầu. Cuối cùng, bạn gửi danh sách các công thức dưới dạng phản hồi JSON bằng cách sử dụng render.

Tiếp theo, bạn sẽ thêm logic để tạo các công thức mới. Cũng giống như khi truy xuất tất cả công thức, bạn sẽ dựa vào ActiveRecord để xác thực và lưu các chi tiết công thức được cung cấp. Hãy cập nhật controller công thức với các dòng mã được đánh dấu sau.

~/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
    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

Trong action create, bạn sử dụng phương thức create của ActiveRecord để tạo một công thức mới. Phương thức create có thể gán tất cả các tham số từ controller được cung cấp vào model cùng một lúc. Phương thức này giúp tạo bản ghi dễ dàng nhưng mở ra khả năng bị sử dụng độc hại. Việc sử dụng độc hại có thể được ngăn chặn bằng cách sử dụng tính năng strong parameters do Rails cung cấp. Bằng cách này, các tham số không thể được gán trừ khi chúng đã được cho phép. Bạn truyền một tham số recipe\_params vào phương thức create trong mã của mình. recipe\\_params là một phương thức private, nơi bạn cho phép các tham số của controller để ngăn nội dung sai hoặc độc hại xâm nhập vào cơ sở dữ liệu của bạn. Trong trường hợp này, bạn cho phép các tham số name, image, ingredientsinstruction để sử dụng hợp lệ với phương thức create.

Controller recipe của bạn hiện có thể đọc và tạo công thức. Tất cả những gì còn lại là logic để đọc và xóa một công thức duy nhất. Hãy cập nhật controller recipes của bạn với đoạn mã được đánh dấu:

~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb
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

Trong các dòng mã mới, bạn tạo một phương thức private có tên là set_recipe, được gọi bởi before_action chỉ khi các action showdelete khớp với một yêu cầu. Phương thức set_recipe sử dụng phương thức find của ActiveRecord để tìm một công thức có id trùng với id được cung cấp trong params và gán nó cho biến instance @recipe. Trong action show, bạn trả về đối tượng @recipe được thiết lập bởi phương thức set_recipe dưới dạng phản hồi JSON.

Trong action destroy, bạn làm điều gì đó tương tự bằng cách sử dụng toán tử điều hướng an toàn &. của Ruby, giúp tránh lỗi nil khi gọi một phương thức. Việc bổ sung này cho phép bạn chỉ xóa một công thức nếu nó tồn tại, sau đó gửi một thông báo dưới dạng phản hồi.

Sau khi thực hiện các thay đổi này đối với recipes_controller.rb, hãy lưu và đóng tệp.

Trong bước này, bạn đã tạo một model và controller cho các công thức. Bạn đã viết đầy đủ logic cần thiết để làm việc với công thức ở phía backend. Trong phần tiếp theo, bạn sẽ tạo các thành phần (component) để hiển thị các công thức.

Bước 7 – Xem các công thức

Trong phần này, bạn sẽ tạo các thành phần để xem các công thức. Bạn sẽ tạo hai trang: một trang để xem tất cả các công thức hiện có và một trang để xem từng công thức riêng lẻ.

Bạn sẽ bắt đầu bằng cách tạo một trang để xem tất cả các công thức. Trước khi tạo trang này, bạn cần có sẵn một số công thức để làm việc, vì cơ sở dữ liệu của bạn hiện đang trống. Rails cung cấp một cách để tạo dữ liệu mẫu (seed data) cho ứng dụng của bạn.

Mở tệp seed có tên là seeds.rb để chỉnh sửa:

$ nano ~/rails_react_recipe/db/seeds.rb

Hãy thay thế toàn bộ nội dung ban đầu của tệp seed bằng đoạn mã sau:

~/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

Trong đoạn mã này, bạn sử dụng một vòng lặp yêu cầu Rails tạo chín công thức với các phần gồm nameingredients và instruction. Hãy lưu và thoát khỏi tệp.

Để nạp dữ liệu này vào cơ sở dữ liệu, hãy chạy lệnh sau trong terminal của bạn:

$ rails db:seed

Việc chạy lệnh này sẽ thêm chín công thức vào cơ sở dữ liệu của bạn. Giờ đây, bạn có thể truy xuất chúng và hiển thị lên giao diện phía frontend.

Component dùng để xem tất cả các công thức sẽ gửi một yêu cầu HTTP đến action index trong RecipesController để lấy danh sách tất cả công thức. Các công thức này sau đó sẽ được hiển thị dưới dạng thẻ (cards) trên trang.

Hãy tạo một tệp có tên Recipes.jsx trong thư mục app/javascript/components bằng lệnh sau:

$ nano ~/rails_react_recipe/app/javascript/components/Recipes.jsx

Khi tệp đã được mở, hãy nhập (import) các module React, useState, useEffect, Link, và useNavigate bằng cách thêm các dòng sau vào đầu tệp:

~/rails_react_recipe/app/javascript/components/Recipes.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";

Tiếp theo, hãy thêm các dòng được đánh dấu để tạo và xuất một functional React component có tên là Recipes.

~/rails_react_recipe/app/javascript/components/Recipes.jsx
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;

Bên trong component Recipe, API điều hướng của React Router sẽ gọi hook useNavigate. Hook useState của React sẽ khởi tạo trạng thái recipes là một mảng rỗng [], cùng với hàm setRecipes để cập nhật trạng thái recipes.

Tiếp theo, trong hook useEffect, bạn sẽ thực hiện một yêu cầu HTTP để lấy tất cả các công thức nấu ăn của mình. Để làm điều này, hãy thêm các dòng được đánh dấu:

~/rails_react_recipe/app/javascript/components/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("/"));
  }, []);
};

export default Recipes;

Trong hook useEffect của bạn, bạn thực hiện một cuộc gọi HTTP để lấy tất cả các công thức nấu ăn bằng cách sử dụng Fetch API. Nếu phản hồi thành công, ứng dụng sẽ lưu mảng các công thức nấu ăn vào trạng thái recipes. Nếu có lỗi xảy ra, nó sẽ chuyển hướng người dùng về trang chủ. Cuối cùng, trả về markup cho các phần tử sẽ được đánh giá và hiển thị trên trang trình duyệt khi component được render. Trong trường hợp này, component sẽ render một thẻ (card) các công thức nấu ăn từ trạng thái recipes. Thêm các dòng được đánh dấu vào Recipes.jsx:

~/rails_react_recipe/app/javascript/components/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;

Lưu và đóng tệp Recipes.jsx.

Giờ đây bạn đã tạo xong một component để hiển thị tất cả các công thức, tiếp theo bạn sẽ tạo một route cho nó. Hãy mở tệp định tuyến frontend tại đường dẫn app/javascript/routes/index.jsx:

$ nano app/javascript/routes/index.jsx

Thêm các dòng được đánh dấu vào tệp:

~/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";
import Recipes from "../components/Recipes";

export default (
  <Router>
    <Routes>
      <Route path="/" exact component={Home} />
      <Route path="/recipes" element={<Recipes />} />
    </Routes>
  </Router>
);

Lưu và thoát khỏi tệp.

Tại thời điểm này, bạn nên xác minh rằng mã của mình đang hoạt động như mong đợi. Như bạn đã làm trước đây, hãy sử dụng lệnh sau để khởi động máy chủ của bạn:

$ bin/dev

Sau đó, hãy mở ứng dụng trong trình duyệt của bạn. Nhấn nút View Recipe trên trang chủ để truy cập trang hiển thị chứa các công thức đã được tạo từ dữ liệu seed:

thiết lập Ruby on Rails v7 trên Ubuntu 20.04

Sử dụng tổ hợp phím CTRL+C trong terminal để dừng máy chủ và quay lại dấu nhắc lệnh.

Bây giờ, khi bạn đã có thể xem tất cả các công thức trong ứng dụng của mình, đã đến lúc tạo một component thứ hai để xem từng công thức riêng lẻ.

Hãy tạo một tệp có tên Recipe.jsx trong thư mục app/javascript/components bằng lệnh sau:

$ nano app/javascript/components/Recipe.jsx

Tương tự như với thành phần Recipes, hãy nhập các mô-đun React, useState, useEffect, Link, useNavigateuseParam bằng cách thêm các dòng sau:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";

Tiếp theo, hãy thêm các dòng được đánh dấu để tạo và xuất một component React dạng hàm có tên là Recipe:

~/rails_react_recipe/app/javascript/components/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: "" });
};

export default Recipe;

Giống như component Recipes, bạn khởi tạo điều hướng của React Router bằng hook useNavigate. Một state recipe và hàm setRecipe sẽ cập nhật state này thông qua hook useState. Ngoài ra, bạn gọi hook useParams, nó sẽ trả về một đối tượng với các cặp key/value tương ứng với các tham số trên URL.

Để tìm một công thức cụ thể, ứng dụng của bạn cần biết id của công thức, nghĩa là component Recipe của bạn sẽ mong đợi một id param trong URL. Bạn có thể truy cập điều này thông qua đối tượng params, đối tượng này chứa giá trị trả về của hook useParams.

Tiếp theo, khai báo một hook useEffect, nơi bạn sẽ truy cập tham số id từ đối tượng params. Sau khi lấy được id của công thức, bạn sẽ thực hiện một yêu cầu HTTP để truy xuất công thức đó. Thêm các dòng được đánh dấu vào tệp của bạn:

~/rails_react_recipe/app/javascript/components/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]);
};

export default Recipe;

Trong hook useEffect, bạn sử dụng giá trị params.id để thực hiện một yêu cầu HTTP GET nhằm truy xuất công thức có id tương ứng, sau đó lưu nó vào state của component bằng hàm setRecipe. Ứng dụng sẽ chuyển hướng người dùng đến trang recipes nếu công thức không tồn tại.

Tiếp theo, thêm một hàm addHtmlEntities, hàm này sẽ được sử dụng để thay thế các thực thể ký tự bằng HTML entities  trong component. Hàm addHtmlEntities sẽ nhận một chuỗi và thay thế tất cả dấu ngoặc mở và đóng đã bị escape bằng các thực thể HTML tương ứng. Hàm này sẽ giúp bạn chuyển đổi các ký tự đã bị escape khi lưu trong phần hướng dẫn của công thức. Thêm các dòng được đánh dấu:

~/rails_react_recipe/app/javascript/components/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(/&lt;/g, "<").replace(/&gt;/g, ">");
  };
};

export default Recipe;

Cuối cùng, trả về markup để hiển thị công thức từ trạng thái của component lên trang, bằng cách thêm các dòng được đánh dấu sau:

~/rails_react_recipe/app/javascript/components/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(/&lt;/g, "<").replace(/&gt;/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;

Với một hàm ingredientList, bạn tách các nguyên liệu trong công thức được phân cách bằng dấu phẩy thành một mảng và sử dụng map để tạo danh sách các nguyên liệu. Nếu không có nguyên liệu nào, ứng dụng sẽ hiển thị một thông báo “No ingredients available”. Bạn cũng thay thế tất cả dấu ngoặc mở và đóng trong phần hướng dẫn của công thức bằng cách truyền nó qua hàm addHtmlEntities. Cuối cùng, đoạn mã sẽ hiển thị hình ảnh của công thức như một ảnh nền chính, thêm nút Delete Recipe bên cạnh phần hướng dẫn, và thêm một nút liên kết quay lại trang recipes.

Lưu ý: Việc sử dụng thuộc tính dangerouslySetInnerHTML của React tiềm ẩn rủi ro vì nó có thể khiến ứng dụng của bạn dễ bị tấn công cross-site scripting (XSS). Tuy nhiên, rủi ro này được giảm thiểu bằng cách đảm bảo rằng các ký tự đặc biệt được nhập khi tạo công thức được thay thế bằng cách sử dụng hàm stripHtmlEntities đã được khai báo trong component NewRecipe.

Lưu và thoát khỏi tệp.

Để hiển thị component Recipe trên một trang, bạn sẽ thêm nó vào tệp định tuyến (routes). Mở tệp định tuyến của bạn để chỉnh sửa:

$ nano app/javascript/routes/index.jsx

Thêm các dòng được đánh dấu sau vào tệp:

~/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";
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>
);

Bạn đã nhập component Recipe vào tệp định tuyến này và thêm một route mới cho nó. Route này có một :id param sẽ được thay thế bằng ID của công thức mà bạn muốn xem.

Lưu và đóng tệp.

Sử dụng script bin/dev để khởi động lại máy chủ của bạn, sau đó truy cập http://localhost:3000 trong trình duyệt. Nhấp vào nút View Recipes để điều hướng đến trang công thức. Trên trang công thức, truy cập bất kỳ công thức nào bằng cách nhấp vào nút “View Recipe” của nó. Bạn sẽ được chào đón bằng một trang được điền dữ liệu từ cơ sở dữ liệu của mình:

Ruby on Rails v7 trên Ubuntu 20.04

Bạn có thể dừng máy chủ bằng cách nhấn CTRL+C.

Trong bước này, bạn đã thêm chín công thức vào cơ sở dữ liệu và tạo các component để xem các công thức này, cả riêng lẻ và dưới dạng một bộ sưu tập. Ở bước tiếp theo, bạn sẽ thêm một component để tạo công thức mới.

Bước 8 – Tạo công thức

Bước tiếp theo để hoàn thiện ứng dụng công thức nấu ăn là khả năng tạo công thức mới. Trong bước này, bạn sẽ tạo một component React cho tính năng này. Component đó sẽ bao gồm biểu mẫu (form) để thu thập thông tin từ người dùng, sau đó gửi yêu cầu đến action create trong RecipesController để lưu dữ liệu công thức vào cơ sở dữ liệu.

Tạo một tệp NewRecipe.jsx trong thư mục app/javascript/components:

$ nano app/javascript/components/NewRecipe.jsx

Trong tệp mới, hãy nhập các mô-đun React, useState, LinkuseNavigate mà bạn đã sử dụng trong các component khác:

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";

Tiếp theo, tạo và xuất một component NewRecipe dạng hàm bằng cách thêm các dòng được đánh dấu:

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx
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;

Tương tự như các component trước, bạn khởi tạo điều hướng của React Router bằng hook useNavigate, sau đó sử dụng hook useState để khởi tạo các state name, ingredientsinstruction, mỗi state có một hàm cập nhật tương ứng. Đây là các trường thông tin bạn cần để tạo một công thức hợp lệ.

Tiếp theo, hãy tạo một hàm stripHtmlEntities để chuyển đổi các ký tự đặc biệt (như <) thành các giá trị đã được mã hóa (như &lt;). Để thực hiện điều này, hãy thêm các dòng được đánh dấu vào component NewRecipe.

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx
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, "&lt;")
      .replace(/>/g, "&gt;");
  };
};

export default NewRecipe;

Trong hàm stripHtmlEntities, bạn sẽ thay thế các ký tự <> bằng các giá trị đã được thoát (escaped values) của chúng. Bằng cách này, bạn sẽ không lưu trữ HTML thô trong cơ sở dữ liệu của mình.

Tiếp theo, thêm các dòng được đánh dấu để bổ sung các hàm onChangeonSubmit vào component NewRecipe nhằm xử lý việc chỉnh sửa và gửi biểu mẫu:

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx
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, "&lt;")
      .replace(/>/g, "&gt;");
  };

  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;

Trong hàm onChange, bạn chấp nhận sự kiện đầu vào của người dùng event và hàm thiết lập trạng thái, sau đó nó cập nhật trạng thái với giá trị đầu vào của người dùng. Trong hàm onSubmit, bạn kiểm tra xem không có đầu vào bắt buộc nào bị trống. Sau đó, bạn xây dựng một đối tượng chứa các tham số cần thiết để tạo một công thức mới. Sử dụng hàm stripHtmlEntities, bạn thay thế các ký tự <> trong hướng dẫn công thức bằng giá trị đã được thoát của chúng và thay thế mọi ký tự xuống dòng mới bằng thẻ ngắt dòng, nhờ đó giữ lại định dạng văn bản mà người dùng đã nhập. Cuối cùng, bạn thực hiện một yêu cầu HTTP POST để tạo công thức mới và chuyển hướng đến trang của nó khi nhận được phản hồi thành công.

Để bảo vệ chống lại các cuộc tấn công Cross-Site Request Forgery (CSRF), Rails đính kèm một mã thông báo bảo mật CSRF vào tài liệu HTML. Mã thông báo này được yêu cầu bất cứ khi nào một yêu cầu không phải là GET được thực hiện. Với hằng số token trong đoạn mã trước, ứng dụng của bạn xác minh mã thông báo trên máy chủ và ném ra một ngoại lệ nếu mã thông báo bảo mật không khớp với những gì được mong đợi. Trong hàm onSubmit, ứng dụng truy xuất mã thông báo CSRF được nhúng trong tài liệu HTML của bạn bởi Rails và sau đó thực hiện một yêu cầu HTTP với một chuỗi JSON. Nếu công thức được tạo thành công, ứng dụng sẽ chuyển hướng người dùng đến trang công thức nơi họ có thể xem công thức mới tạo của mình.

Cuối cùng, trả về markup hiển thị một biểu mẫu để người dùng nhập chi tiết cho công thức mà người dùng muốn tạo. Thêm các dòng được đánh dấu:

~/rails_react_recipe/app/javascript/components/NewRecipe.jsx
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, "&lt;")
      .replace(/>/g, "&gt;");
  };

  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;

Markup được trả về bao gồm một biểu mẫu chứa ba trường nhập liệu; mỗi trường dành cho recipeName, recipeIngredientsinstruction. Mỗi trường nhập liệu có một trình xử lý sự kiện onChange gọi hàm onChange. Một trình xử lý sự kiện onSubmit cũng được gắn vào nút gửi và gọi hàm onSubmit để gửi dữ liệu biểu mẫu.

Lưu và thoát khỏi tệp.

Để truy cập component này trong trình duyệt, hãy cập nhật tệp định tuyến của bạn với route của nó:

$ nano app/javascript/routes/index.jsx

Cập nhật tệp định tuyến của bạn để bao gồm các dòng được đánh dấu này:

~/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";
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>
);

Với route đã được thiết lập, hãy lưu và thoát khỏi tệp của bạn.

Khởi động lại máy chủ phát triển của bạn và truy cập http://localhost:3000 trong trình duyệt. Điều hướng đến trang công thức và nhấp vào nút Create New Recipe. Bạn sẽ thấy một trang với một biểu mẫu để thêm công thức vào cơ sở dữ liệu của mình:

Nhập các chi tiết công thức cần thiết và nhấp vào nút Create Recipe. Công thức mới tạo sau đó sẽ xuất hiện trên trang. Khi hoàn tất, hãy đóng máy chủ.

Trong bước này, bạn đã thêm khả năng tạo công thức vào ứng dụng công thức nấu ăn của mình. Trong bước tiếp theo, bạn sẽ thêm chức năng xóa công thức.

Bước 9 – Xóa công thức

Trong phần này, bạn sẽ sửa đổi component Recipe của mình để bao gồm tùy chọn xóa công thức. Khi bạn nhấp vào nút xóa trên trang công thức, ứng dụng sẽ gửi yêu cầu xóa một công thức khỏi cơ sở dữ liệu.

Đầu tiên, hãy mở tệp Recipe.jsx của bạn để chỉnh sửa:

$ nano app/javascript/components/Recipe.jsx

Trong component Recipe, hãy thêm hàm deleteRecipe với các dòng được đánh dấu sau:

~/rails_react_recipe/app/javascript/components/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(/&lt;/g, "<").replace(/&gt;/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="">
...

Trong hàm deleteRecipe, bạn lấy ID của công thức cần xóa, sau đó xây dựng URL và lấy CSRF token. Tiếp theo, bạn thực hiện một yêu cầu DELETE đến Recipes controller để xóa công thức. Ứng dụng sẽ chuyển hướng người dùng đến trang công thức nếu công thức được xóa thành công.

Để chạy mã trong hàm deleteRecipe bất cứ khi nào nút xóa được nhấp, hãy truyền nó làm trình xử lý sự kiện onClick cho nút. Thêm sự kiện onClick vào phần tử nút xóa trong component:

~/rails_react_recipe/app/javascript/components/Recipe.jsx
...
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>
  );
...

Tại thời điểm này trong hướng dẫn, tệp Recipe.jsx hoàn chỉnh của bạn phải khớp với tệp này:

~/rails_react_recipe/app/javascript/components/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(/&lt;/g, "<").replace(/&gt;/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;

Lưu và thoát khỏi tệp. Khởi động lại máy chủ ứng dụng và điều hướng đến trang chủ. Nhấp vào nút View Recipes để truy cập tất cả các công thức hiện có, sau đó mở bất kỳ công thức cụ thể nào và nhấp vào nút Delete Recipe trên trang để xóa bài viết. Bạn sẽ được chuyển hướng đến trang công thức, và công thức đã xóa sẽ không còn tồn tại. Với nút xóa đã hoạt động, giờ đây bạn đã có một ứng dụng công thức nấu ăn hoạt động đầy đủ chức năng!

Kết luận

Trong hướng dẫn này, bạn đã tạo một ứng dụng công thức nấu ăn với Ruby on Rails và giao diện người dùng React, sử dụng PostgreSQL làm cơ sở dữ liệu và Bootstrap để tạo kiểu. Nếu bạn muốn tiếp tục xây dựng với Ruby on Rails, hãy cân nhắc mở rộng ứng dụng bằng cách bổ sung tính năng chỉnh sửa công thức, phân loại theo danh mục hoặc tích hợp xác thực người dùng.

0 Bình luận

Đăng nhập để thảo luận

Chuyên mục Hướng dẫn

Tổng hợp các bài viết hướng dẫn, nghiên cứu và phân tích chi tiết về kỹ thuật, các xu hướng công nghệ mới nhất dành cho lập trình viên.

Đăng ký nhận bản tin của chúng tôi

Hãy trở thành người nhận được các nội dung hữu ích của CyStack sớm nhất

Xem chính sách của chúng tôi Chính sách bảo mật.

Đăng ký nhận Newsletter

Nhận các nội dung hữu ích mới nhất