Cómo configurar un proyecto de Ruby on Rails v7 con un frontend de React en Ubuntu 20.04

El autor seleccionó la Electronic Frontier Foundation para recibir una donación como parte del programa Write for DOnations.

Introducción

Ruby on Rails es un popular framework de aplicación web del lado del servidor. Impulsa muchas aplicaciones populares que existen en la web hoy en día, como GitHub, Basecamp, SoundCloud, Airbnb y Twitch. Con su énfasis en la experiencia del programador y la apasionada comunidad construida en torno a él, Ruby on Rails le proporcionará las herramientas que necesita para construir y mantener su aplicación web moderna.

React es una biblioteca de JavaScript utilizada para crear interfaces de usuario de front-end. Respaldada por Facebook, es una de las bibliotecas de front-end más populares utilizadas en la web hoy en día. React ofrece características como un Modelo de Objeto de Documento (DOM) virtual, arquitectura de componentes y gestión de estado, que hacen que el proceso de desarrollo de front-end sea más organizado y eficiente.

Con el frontend web moviéndose hacia frameworks separados del código del lado del servidor, combinar la elegancia de Rails con la eficiencia de React te permitirá construir aplicaciones potentes y modernas informadas por las tendencias actuales. Al utilizar React para renderizar componentes desde dentro de una vista de Rails (en lugar del motor de plantillas de Rails), tu aplicación se beneficiará de los últimos avances en JavaScript y desarrollo frontend, aprovechando la expresividad de Ruby on Rails.

En este tutorial, crearás una aplicación de Ruby on Rails que almacena tus recetas favoritas y luego las muestra con un frontend de React. Cuando hayas terminado, podrás crear, ver y eliminar recetas utilizando una interfaz de React estilizada con Bootstrap:

Requisitos previos

Para seguir este tutorial, necesitas:

Nota: La versión 7 de Rails no es compatible con versiones anteriores. Si estás utilizando la versión 5 de Rails, por favor visita el tutorial para Cómo Configurar un Proyecto de Ruby on Rails v5 con un Frontend de React en Ubuntu 18.04.

Paso 1 — Crear una nueva aplicación Rails

Construirás tu aplicación de recetas en el marco de aplicación Rails en este paso. Primero, crearás una nueva aplicación Rails, que se configurará para funcionar con React.

Rails proporciona varios scripts llamados generadores que crean todo lo necesario para construir una aplicación web moderna. Para revisar una lista completa de estos comandos y lo que hacen, ejecuta el siguiente comando en tu terminal:

  1. rails -h

Este comando generará una lista exhaustiva de opciones, permitiéndote establecer los parámetros de tu aplicación. Uno de los comandos listados es el comando new, que crea una nueva aplicación de Rails.

Ahora, crearás una nueva aplicación de Rails utilizando el generador new. Ejecuta el siguiente comando en tu terminal:

  1. rails new rails_react_recipe -d postgresql -j esbuild -c bootstrap -T

El comando anterior crea una nueva aplicación de Rails en un directorio llamado rails_react_recipe, instala las dependencias requeridas de Ruby y JavaScript, y configura Webpack. Las banderas asociadas con este comando generador new incluyen lo siguiente:

  • La bandera -d especifica el motor de base de datos preferido, que en este caso es PostgreSQL.
  • La bandera -j especifica el enfoque de JavaScript de la aplicación. Rails ofrece algunas formas diferentes de manejar el código JavaScript en las aplicaciones de Rails. La opción esbuild pasada a la bandera -j instruye a Rails a preconfigurar esbuild como el empaquetador de JavaScript preferido.
  • La bandera -c especifica el procesador de CSS de la aplicación. Bootstrap es la opción preferida en este caso.
  • La bandera -T instruye a Rails a omitir la generación de archivos de prueba ya que no escribirás pruebas para este tutorial. Este comando también se sugiere si deseas usar una herramienta de prueba de Ruby diferente de la que proporciona Rails.

Una vez que el comando haya terminado, dirígete al directorio rails_react_recipe, que es el directorio raíz de tu aplicación:

  1. cd rails_react_recipe

A continuación, enumera el contenido del directorio:

  1. ls

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

Este directorio raíz tiene varios archivos y carpetas generados automáticamente que conforman la estructura de una aplicación Rails, incluido un archivo package.json que contiene las dependencias de una aplicación React.

Ahora que ha creado correctamente una nueva aplicación Rails, la conectará a una base de datos en el siguiente paso.

Paso 2: Configuración de la base de datos

Antes de ejecutar su nueva aplicación Rails, primero debe conectarla a una base de datos. En este paso, conectará la aplicación Rails recién creada a una base de datos PostgreSQL para que los datos de las recetas se puedan almacenar y recuperar según sea necesario.

El archivo database.yml encontrado en config/database.yml contiene detalles de la base de datos como nombres de base de datos para diferentes entornos de desarrollo. Rails especifica un nombre de base de datos para los diversos entornos de desarrollo agregando un guion bajo (_) seguido del nombre del entorno. En este tutorial, usará los valores de configuración de base de datos predeterminados, pero puede cambiar sus valores de configuración si es necesario.

Nota: En este punto, puedes alterar config/database.yml para establecer qué rol de PostgreSQL te gustaría que Rails use para crear tu base de datos. Durante los requisitos previos, creaste un rol que está asegurado por una contraseña en el tutorial Cómo Usar PostgreSQL con tu Aplicación Ruby on Rails. Si aún no has configurado el usuario, ahora puedes seguir las instrucciones para Paso 4 — Configurar y Crear tu Base de Datos en el mismo tutorial previo.

Rails ofrece muchos comandos que facilitan el desarrollo de aplicaciones web, incluidos comandos para trabajar con bases de datos como create, drop y reset. Para crear una base de datos para tu aplicación, ejecuta el siguiente comando en tu terminal:

  1. rails db:create

Este comando crea una base de datos development y test, dando como resultado la siguiente salida:

Output
Created database 'rails_react_recipe_development' Created database 'rails_react_recipe_test'

Ahora que la aplicación está conectada a una base de datos, inicia la aplicación ejecutando el siguiente comando:

  1. bin/dev

Rails proporciona un script alternativo bin/dev que inicia una aplicación Rails ejecutando los comandos en el archivo Procfile.dev en el directorio raíz de la aplicación utilizando la gema Foreman.

Una vez que ejecutes este comando, tu indicador de comando desaparecerá y en su lugar se imprimirá la siguiente salida:

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.

Para acceder a tu aplicación, abre una ventana del navegador y navega a http://localhost:3000. Se cargará la página de bienvenida predeterminada de Rails, lo que significa que has configurado correctamente tu aplicación de Rails:

Para detener el servidor web, presiona CTRL+C en la terminal donde se está ejecutando el servidor. Obtendrás un mensaje de despedida de 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

Luego, volverá a aparecer el prompt de tu terminal.

Has configurado correctamente una base de datos para tu aplicación de recetas de comida. En el siguiente paso, instalarás las dependencias de JavaScript que necesitas para armar tu frontend de React.

Paso 3 — Instalación de Dependencias del Frontend

En este paso, instalarás las dependencias de JavaScript necesarias en el frontend de tu aplicación de recetas de comida. Incluyen:

  • React para construir interfaces de usuario.
  • React DOM para permitir que React interactúe con el DOM del navegador.
  • React Router para manejar la navegación en una aplicación de React.

Ejecuta el siguiente comando para instalar estos paquetes con el gestor de paquetes Yarn:

  1. yarn add react react-dom react-router-dom

Este comando utiliza Yarn para instalar los paquetes especificados y los añade al archivo package.json. Para verificar esto, abre el archivo package.json ubicado en el directorio raíz del proyecto:

  1. nano package.json

Los paquetes instalados se listarán bajo la clave 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"
  }
}

Cierra el archivo presionando CTRL+X.

Has instalado algunas dependencias front-end para tu aplicación. A continuación, configurarás una página de inicio para tu aplicación de recetas de comida.

Paso 4 — Configuración de la Página de Inicio

Con las dependencias requeridas instaladas, ahora crearás una página de inicio para la aplicación para que sirva como la página de inicio cuando los usuarios visiten por primera vez la aplicación.

Rails sigue el patrón arquitectónico Modelo-Vista-Controlador para aplicaciones. En el patrón MVC, el propósito de un controlador es recibir solicitudes específicas y pasarlas al modelo o vista apropiados. Actualmente, la aplicación muestra la página de bienvenida de Rails cuando se carga la URL raíz en el navegador. Para cambiar esto, crearás un controlador y una vista para la página de inicio y luego lo emparejarás con una ruta.

Rails proporciona un generador de controller para crear un controlador. El generador de controller recibe un nombre de controlador y una acción correspondiente. Para obtener más información al respecto, puedes revisar la documentación de Rails.

Este tutorial llamará al controlador Homepage. Ejecuta el siguiente comando para crear un controlador Homepage con una acción index:

  1. rails g controller Homepage index

Nota:
En Linux, el error FATAL: Listen error: unable to monitor directories for changes. puede deberse a un límite del sistema en la cantidad de archivos que tu máquina puede monitorear para realizar cambios. Ejecuta el siguiente comando para solucionarlo:

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

Este comando aumentará permanentemente la cantidad de directorios que puedes monitorear con Listen a 524288. Puedes cambiar esto nuevamente ejecutando el mismo comando y reemplazando 524288 con tu número deseado.

Al ejecutar el comando controller, se generan los siguientes archivos:

  • A homepage_controller.rb file for receiving all homepage-related requests. This file contains the index action you specified in the command.
  • A homepage_helper.rb file for adding helper methods related to the Homepage controller.
  • Un archivo index.html.erb como la página de vista para representar cualquier cosa relacionada con la página de inicio.

Además de estas nuevas páginas creadas al ejecutar el comando de Rails, Rails también actualiza tu archivo de rutas ubicado en config/routes.rb, agregando una ruta get para tu página de inicio, que modificarás como tu ruta principal.

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:

  1. nano config/routes.rb

En este archivo, reemplaza get 'homepage/index' con root 'homepage#index' para que el archivo coincida con lo siguiente:

~/rails_react_recipe/config/routes.rb
Rails.application.routes.draw do
  root 'homepage#index'
  # Para obtener detalles sobre el DSL disponible dentro de este archivo, consulta http://guides.rubyonrails.org/routing.html
end

Esta modificación instruye a Rails para que mapee las solicitudes a la raíz de la aplicación a la acción index del controlador Homepage, que a su vez renderiza en el navegador lo que sea que esté en el archivo index.html.erb ubicado en app/views/homepage/index.html.erb.

Guarda y cierra el archivo.

Para verificar que esto funcione, inicia tu aplicación:

  1. bin/dev

Cuando abras o actualices la aplicación en el navegador, se cargará una nueva página de inicio para tu aplicación:

Una vez que hayas verificado que tu aplicación funciona, presiona CTRL+C para detener el servidor.

A continuación, abre el archivo ~/rails_react_recipe/app/views/homepage/index.html.erb:

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

Elimina el código dentro del archivo, luego guarda el archivo como vacío. Al hacer esto, te aseguras de que el contenido de index.html.erb no interfiera con la representación de React en tu frontend.

Ahora que has configurado tu página de inicio para tu aplicación, puedes pasar a la siguiente sección, donde configurarás el frontend de tu aplicación para usar React.

Paso 5 — Configurar React como tu frontend de Rails

En este paso, configurarás Rails para usar React en el frontend de la aplicación, en lugar de su motor de plantillas. Esta nueva configuración te permitirá crear una página de inicio más atractiva visualmente con React.

Con la ayuda de la opción esbuild especificada al generar la aplicación de Rails, la mayoría de la configuración requerida para permitir que JavaScript funcione sin problemas con Rails ya está en su lugar. Todo lo que queda es cargar el punto de entrada de la aplicación React en el punto de entrada de esbuild para los archivos JavaScript. Para hacer esto, comienza creando un directorio de componentes en el directorio app/javascript:

  1. mkdir ~/rails_react_recipe/app/javascript/components

El directorio components albergará el componente para la página de inicio, junto con otros componentes de React en la aplicación, incluido el archivo de entrada en la aplicación React.

A continuación, abre el archivo application.js ubicado en app/javascript/application.js:

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

Agrega la línea de código resaltada al archivo:

~/rails_react_recipe/app/javascript/application.js
// Punto de entrada para el script de compilación en tu package.json
import "@hotwired/turbo-rails"
import "./controllers"
import * as bootstrap from "bootstrap"
import "./components"

La línea de código agregada al archivo application.js importará el código en el archivo de entrada index.jsx, haciéndolo disponible para esbuild para el empaquetado. Con el directorio /components importado en el punto de entrada JavaScript de la aplicación Rails, puedes crear un componente React para tu página de inicio. La página de inicio contendrá algunos textos y un botón de llamada a la acción para ver todas las recetas.

Guarda y cierra el archivo.

Luego, crea un archivo Home.jsx en el directorio components:

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

Agrega el siguiente código al archivo:

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

En este código, importas React y el componente Link de React Router. El componente Link crea un hipervínculo para navegar de una página a otra. Luego, creas y exportas un componente funcional que contiene algo de lenguaje de marcado para tu página de inicio, estilizado con clases de Bootstrap.

Guarda y cierra el archivo.

Con tu componente Home configurado, ahora configurarás el enrutamiento usando React Router. Crea un directorio routes en el directorio app/javascript:

  1. mkdir ~/rails_react_recipe/app/javascript/routes

El directorio routes contendrá algunas rutas con sus componentes correspondientes. Cada vez que se cargue una ruta especificada, se representará su componente correspondiente en el navegador.

En el directorio routes, crea un archivo index.jsx:

  1. nano ~/rails_react_recipe/app/javascript/routes/index.jsx

Agrega el siguiente código a él:

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

En este archivo de ruta index.jsx, importas los siguientes módulos: el módulo React que te permite usar React, así como los módulos BrowserRouter, Routes y Route de React Router, que juntos te ayudan a navegar de una ruta a otra. Por último, importas tu componente Home, que se renderizará cada vez que una solicitud coincida con la ruta raíz (/). Cuando desees agregar más páginas a tu aplicación, puedes declarar una ruta en este archivo y emparejarla con el componente que deseas renderizar para esa página.

Guarda y cierra el archivo.

Ahora has configurado el enrutamiento utilizando React Router. Para que React sea consciente de las rutas disponibles y las utilice, estas deben estar disponibles en el punto de entrada de la aplicación. Para lograr esto, renderizarás tus rutas en un componente que React representará en tu archivo de entrada.

Crea un archivo App.jsx en el directorio app/javascript/components:

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

Agrega el siguiente código al archivo App.jsx:

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

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

En el archivo App.jsx, importas React y los archivos de ruta que acabas de crear. Luego exportas un componente para renderizar las rutas dentro de fragmentos. Este componente se representará en el punto de entrada de la aplicación, haciendo que las rutas estén disponibles cada vez que se carga la aplicación.

Guarda y cierra el archivo.

Ahora que has configurado tu App.jsx, puedes renderizarlo en tu archivo de entrada. Crea un archivo index.jsx en el directorio components:

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

Agrega el siguiente código al archivo index.js:

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

En las líneas de import, importas la biblioteca React, la función createRoot de ReactDOM y tu componente App. Utilizando la función createRoot de ReactDOM, creas un elemento raíz como un elemento div añadido a la página y renderizas tu componente App en él. Cuando se carga la aplicación, React representará el contenido del componente App dentro del elemento div en la página.

Guarda y cierra el archivo.

Finalmente, agregarás algunos estilos CSS a tu página de inicio.

Abre el archivo application.bootstrap.scss en el directorio ~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss:

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

A continuación, reemplaza el contenido del archivo application.bootstrap.scss con el siguiente código:

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

Configura algunos colores personalizados para la página. La sección .hero creará el marco para una imagen heroica, o un gran banner web en la página principal de tu sitio web, que agregarás más tarde. Además, el estilo custom-button.btn dará estilo al botón que el usuario utilizará para ingresar a la aplicación.

Con tus estilos CSS en su lugar, guarda y cierra el archivo.

A continuación, reinicia el servidor web de tu aplicación:

  1. bin/dev

Luego, recarga la aplicación en tu navegador. Se cargará una nueva página de inicio:

Detén el servidor web con CTRL+C.

Configuraste tu aplicación para utilizar React como frontend en este paso. En el siguiente paso, crearás modelos y controladores que te permitirán crear, leer, actualizar y eliminar recetas.

Paso 6: Crear el Controlador y Modelo de Recetas

Ahora que has configurado un frontend de React para tu aplicación, crearás un modelo y controlador de Receta. El modelo de receta representará la tabla de la base de datos que contiene información sobre las recetas del usuario, mientras que el controlador recibirá y manejará las solicitudes para crear, leer, actualizar o eliminar recetas. Cuando un usuario solicita una receta, el controlador de recetas recibe esta solicitud y la pasa al modelo de recetas, que recupera los datos solicitados de la base de datos. Luego, el modelo devuelve los datos de la receta como respuesta al controlador. Finalmente, esta información se muestra en el navegador.

Comienza creando un modelo Receta usando el subcomando generate model proporcionado por Rails y especificando el nombre del modelo junto con sus columnas y tipos de datos. Ejecuta el siguiente comando:

  1. rails generate model Recipe name:string ingredients:text instruction:text image:string

El comando anterior instruye a Rails a crear un modelo Receta junto con una columna nombre de tipo string, una columna ingredientes y instrucción de tipo text, y una columna imagen de tipo string. Este tutorial ha nombrado el modelo Receta, porque los modelos en Rails usan un nombre en singular mientras que sus tablas de base de datos correspondientes usan un nombre en plural.

Al ejecutar el comando generate model se crean dos archivos y se imprime la siguiente salida:

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

Los dos archivos creados son:

  • 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.

A continuación, editarás el archivo del modelo de receta para asegurar que solo se guarden datos válidos en la base de datos. Puedes lograr esto agregando validaciones de base de datos a tu modelo.

Abre tu modelo de receta ubicado en app/models/recipe.rb:

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

Agrega las siguientes líneas de código resaltadas al archivo:

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

En este código, agregas validación del modelo, que verifica la presencia de los campos name, ingredients e instruction. Sin estos tres campos, una receta es inválida y no se guardará en la base de datos.

Guarda y cierra el archivo.

Para que Rails cree la tabla recipes en tu base de datos, debes ejecutar una migración, que es una forma de realizar cambios en tu base de datos de manera programática. Para asegurarte de que la migración funcione con la base de datos que configuraste, debes realizar cambios en el archivo 20221017220817_create_recipes.rb.

Abre este archivo en tu editor:

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

Agrega los materiales resaltados para que tu archivo coincida con lo siguiente:

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

Este archivo de migración contiene una clase Ruby con un método change y un comando para crear una tabla llamada recipes junto con las columnas y sus tipos de datos. También actualizas 20221017220817_create_recipes.rb con una restricción NOT NULL en las columnas name, ingredients e instruction agregando null: false, asegurando que estas columnas tengan un valor antes de cambiar la base de datos. Finalmente, agregas una URL de imagen predeterminada para tu columna de imagen; esta podría ser otra URL si deseas usar una imagen diferente.

Con estos cambios, guarda y sale del archivo. Ahora estás listo para ejecutar tu migración y crear tu tabla. En tu terminal, ejecuta el siguiente comando:

  1. rails db:migrate

Utiliza el comando de migración de base de datos para ejecutar las instrucciones en tu archivo de migración. Una vez que el comando se ejecute correctamente, recibirás una salida similar a la siguiente:

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

Con tu modelo de receta en su lugar, a continuación crearás tu controlador de recetas para agregar la lógica de creación, lectura y eliminación de recetas. Ejecuta el siguiente comando:

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

En este comando, crearás un controlador de Recetas en un directorio api/v1 con acciones de index, create, show y destroy. La acción index manejará la obtención de todas tus recetas; la acción create será responsable de crear nuevas recetas; la acción show obtendrá una sola receta, y la acción destroy contendrá la lógica para eliminar una receta.

También pasas algunas banderas para hacer que el controlador sea más ligero, incluyendo:

  • --skip-template-engine, que indica a Rails que omita la generación de archivos de vista de Rails ya que React maneja tus necesidades de frontend.
  • --no-helper, que indica a Rails que omita la generación de un archivo de ayuda para tu controlador.

Al ejecutar el comando también se actualiza tu archivo de rutas con una ruta para cada acción en el controlador de Recetas.

Cuando se ejecute el comando, imprimirá una salida como esta:

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

Para usar estas rutas, realizarás cambios en tu archivo config/routes.rb. Abre el archivo routes.rb en tu editor de texto:

  1. nano ~/rails_react_recipe/config/routes.rb

Actualiza este archivo para que se vea como el siguiente código, alterando o agregando las líneas resaltadas:

~/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 tus rutas de aplicación según el DSL en https://guides.rubyonrails.org/routing.html

  # Define la ruta de la raíz ("/")
  # root "articles#index"
end

En este archivo de rutas, modifica el verbo HTTP de las rutas create y destroy para que pueda post y delete datos. También modifica las rutas para las acciones show y destroy agregando un parámetro :id a la ruta. :id contendrá el número de identificación de la receta que deseas leer o eliminar.

Agrega una ruta de captura de todo con get '/*path' que dirigirá cualquier otra solicitud que no coincida con las rutas existentes a la acción index del controlador homepage. El enrutamiento del front-end manejará las solicitudes no relacionadas con la creación, lectura o eliminación de recetas.

Guarda y sale del archivo.

Para evaluar una lista de rutas disponibles en tu aplicación, ejecuta el siguiente comando:

  1. rails routes

Al ejecutar este comando, se muestra una lista extensa de patrones de URI, verbos y controladores o acciones coincidentes para tu proyecto.

A continuación, agregarás la lógica para obtener todas las recetas a la vez. Rails utiliza la biblioteca ActiveRecord para manejar tareas relacionadas con la base de datos como esta. ActiveRecord conecta clases con tablas de base de datos relacionales y proporciona una API rica para trabajar con ellas.

Para obtener todas las recetas, utilizarás ActiveRecord para consultar la tabla de recetas y obtener todas las recetas en la base de datos.

Abre el archivo recipes_controller.rb con el siguiente comando:

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

Agrega las líneas resaltadas al controlador de recetas:

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

En tu acción index, utilizas el método all de ActiveRecord para obtener todas las recetas en tu base de datos. Utilizando el método order, las ordenas en orden descendente según su fecha de creación, lo que colocará las recetas más nuevas primero. Por último, envías tu lista de recetas como respuesta JSON con render.

A continuación, agregarás la lógica para crear nuevas recetas. Al igual que al obtener todas las recetas, confiarás en ActiveRecord para validar y guardar los detalles de la receta proporcionados. Actualiza tu controlador de recetas con las siguientes líneas de código resaltadas:

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

En la acción create, utilizas el método create de ActiveRecord para crear una nueva receta. El método create puede asignar todos los parámetros del controlador proporcionados al modelo de una vez. Este método facilita la creación de registros pero abre la posibilidad de un uso malintencionado. El uso malintencionado puede prevenirse utilizando la característica de parámetros fuertes proporcionada por Rails. De esta manera, los parámetros no pueden asignarse a menos que hayan sido permitidos. Pasas un parámetro recipe_params al método create en tu código. El recipe_params es un método private donde permites que los parámetros del controlador eviten que contenido incorrecto o malintencionado llegue a tu base de datos. En este caso, permites un parámetro name, image, ingredients e instruction para el uso válido del método create.

Tu controlador de recetas ahora puede leer y crear recetas. Todo lo que queda es la lógica para leer y eliminar una única receta. Actualiza tu controlador de recetas con el código resaltado:

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

En las nuevas líneas de código, creas un método privado set_recipe llamado por un before_action solo cuando las acciones show y delete coinciden con una solicitud. El método set_recipe utiliza el método find de ActiveRecord para encontrar una receta cuyo id coincida con el id proporcionado en los params y lo asigna a una variable de instancia @recipe. En la acción show, devuelves el objeto @recipe establecido por el método set_recipe como una respuesta JSON.

En la acción destroy, hiciste algo similar usando el operador de navegación segura de Ruby &., que evita errores de nil al llamar a un método. Esta adición te permite eliminar una receta solo si existe, luego enviar un mensaje como respuesta.

Después de realizar estos cambios en recipes_controller.rb, guarda y cierra el archivo.

En este paso, creaste un modelo y un controlador para tus recetas. Has escrito toda la lógica necesaria para trabajar con recetas en el backend. En la próxima sección, crearás componentes para ver tus recetas.

Paso 7 — Visualización de Recetas

En esta sección, crearás componentes para visualizar recetas. Crearás dos páginas: una para ver todas las recetas existentes y otra para ver recetas individuales.

Comenzarás creando una página para ver todas las recetas. Antes de crear la página, necesitas recetas con las que trabajar, ya que tu base de datos está actualmente vacía. Rails proporciona una manera de crear datos de semilla para tu aplicación.

Abre el archivo de semilla llamado seeds.rb para editarlo:

  1. nano ~/rails_react_recipe/db/seeds.rb

Reemplaza el contenido inicial del archivo de semilla con el siguiente código:

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

En este código, utilizas un bucle que instruye a Rails a crear nueve recetas con secciones para name, ingredients, y instruction. Guarda y sale del archivo.

Para poblar la base de datos con estos datos, ejecuta el siguiente comando en tu terminal:

  1. rails db:seed

Al ejecutar este comando, se añaden nueve recetas a tu base de datos. Ahora puedes recuperarlas y mostrarlas en el frontend.

El componente para ver todas las recetas hará una solicitud HTTP a la acción index en el controlador RecipesController para obtener una lista de todas las recetas. Estas recetas luego se mostrarán en tarjetas en la página.

Crea un archivo Recipes.jsx en el directorio app/javascript/components:

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

Una vez que el archivo esté abierto, importa los módulos React, useState, useEffect, Link, y useNavigate agregando las siguientes líneas:

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

A continuación, agrega las líneas resaltadas para crear y exportar un componente funcional de React llamado 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;

Dentro del componente Recipe, la API de navegación de React Router llamará al gancho useNavigate. El gancho useState de React inicializará el estado recipes, que es un array vacío ([]), y una función setRecipes para actualizar el estado recipes.

A continuación, en un gancho useEffect, realizarás una solicitud HTTP para obtener todas tus recetas. Para hacer esto, agrega las líneas resaltadas:

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

En tu gancho useEffect, realizas una llamada HTTP para obtener todas las recetas utilizando la API Fetch. Si la respuesta es exitosa, la aplicación guarda el array de recetas en el estado recipes. Si ocurre un error, redirigirá al usuario a la página de inicio.

Por último, devuelve el marcado para los elementos que se evaluarán y mostrarán en la página del navegador cuando se renderice el componente. En este caso, el componente renderizará una tarjeta de recetas del estado recipes. Agrega las líneas resaltadas a 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;

Guarda y sal de Recipes.jsx.

Ahora que has creado un componente para mostrar todas las recetas, crearás una ruta para ello. Abre el archivo de ruta del frontend app/javascript/routes/index.jsx:

  1. nano app/javascript/routes/index.jsx

Agrega las líneas resaltadas al archivo:

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

Guarda y sale del archivo.

En este punto, es una buena idea verificar que tu código esté funcionando como se espera. Como hiciste antes, utiliza el siguiente comando para iniciar tu servidor:

  1. bin/dev

Luego abre la aplicación en tu navegador. Presiona el botón Ver Receta en la página de inicio para acceder a una página de visualización con tus recetas iniciales:

Utiliza CTRL+C en tu terminal para detener el servidor y volver al prompt.

Ahora que puedes ver todas las recetas en tu aplicación, es hora de crear un segundo componente para ver recetas individuales. Crea un archivo Recipe.jsx en el directorio app/javascript/components:

  1. nano app/javascript/components/Recipe.jsx

Al igual que con el componente Recipes, importa los módulos React, useState, useEffect, Link, useNavigate y useParam agregando las siguientes líneas:

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

A continuación, agrega las líneas resaltadas para crear y exportar un componente funcional de React llamado 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;

Como el componente Recipes, inicializas la navegación de React Router con el gancho useNavigate. Un estado recipe y una función setRecipe actualizarán el estado con el gancho useState. Además, llamas al gancho useParams, que devuelve un objeto cuyos pares clave/valor son de parámetros de URL.

Para encontrar una receta específica, tu aplicación necesita conocer el id de la receta, lo que significa que tu componente Recipe espera un param id en la URL. Puedes acceder a esto a través del objeto params que contiene el valor de retorno del gancho useParams.

Siguiente, declare un gancho useEffect donde accederá al id param del objeto params. Una vez que obtenga el id param de la receta, realizará una solicitud HTTP para recuperarla. Agregue las líneas resaltadas a su archivo:

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

En el gancho useEffect, utiliza el valor params.id para hacer una solicitud HTTP GET y recuperar la receta que posee el id y luego guardarla en el estado del componente usando la función setRecipe. La aplicación redirige al usuario a la página de recetas si la receta no existe.

A continuación, agregue una función addHtmlEntities, que se utilizará para reemplazar las entidades de caracteres con entidades HTML en el componente. La función addHtmlEntities tomará una cadena y reemplazará todos los corchetes escapados de apertura y cierre con sus entidades HTML. Esta función le ayudará a convertir cualquier carácter escapado que se haya guardado en las instrucciones de su receta. Agregue las líneas resaltadas:

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

Finalmente, devuelva el marcado para representar la receta en el estado del componente en la página, agregando las líneas resaltadas:

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

Con una función ingredientList, divides tus ingredientes de receta separados por comas en un array y los mapeas para crear una lista de ingredientes. Si no hay ingredientes, la aplicación muestra un mensaje que dice No hay ingredientes disponibles. También reemplazas todos los corchetes de apertura y cierre en las instrucciones de la receta al pasarlas por la función addHtmlEntities. Por último, el código muestra la imagen de la receta como una imagen destacada, añade un botón Eliminar Receta junto a las instrucciones de la receta, y agrega un botón que enlaza de vuelta a la página de recetas.

Nota: El uso del atributo dangerouslySetInnerHTML de React es arriesgado ya que expone tu aplicación a ataques de cross-site scripting. Este riesgo se reduce asegurando que los caracteres especiales ingresados al crear recetas sean reemplazados usando la función stripHtmlEntities declarada en el componente NewRecipe.

Guarda y cierra el archivo.

Para ver el componente Recipe en una página, lo añadirás a tu archivo de rutas. Abre tu archivo de rutas para editarlo:

  1. nano app/javascript/routes/index.jsx

Añade las siguientes líneas resaltadas al archivo:

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

Importas tu componente Recipe en este archivo de ruta y añades una ruta. Su ruta tiene un param :id que será reemplazado por el id de la receta que deseas ver.

Guarda y cierra el archivo.

Utiliza el script bin/dev para iniciar tu servidor nuevamente, luego visita http://localhost:3000 en tu navegador. Haz clic en el botón Ver Recetas para navegar a la página de recetas. En la página de recetas, accede a cualquier receta haciendo clic en su botón Ver Receta. Serás recibido con una página poblada con los datos de tu base de datos:

Puedes detener el servidor con CTRL+C.

En este paso, agregaste nueve recetas a tu base de datos y creaste componentes para ver estas recetas, tanto individualmente como en conjunto. En el próximo paso, agregarás un componente para crear recetas.

Paso 8 — Creando Recetas

El siguiente paso para tener una aplicación de recetas de comida utilizable es la capacidad de crear nuevas recetas. En este paso, crearás un componente para esta función. El componente contendrá un formulario para recopilar los detalles de la receta requeridos por el usuario y luego realizará una solicitud a la acción create en el controlador Recipe para guardar los datos de la receta.

Crea un archivo NewRecipe.jsx en el directorio app/javascript/components:

  1. nano app/javascript/components/NewRecipe.jsx

En el nuevo archivo, importa los módulos React, useState, Link y useNavigate que utilizaste en otros componentes:

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

A continuación, crea y exporta un componente funcional NewRecipe añadiendo las líneas resaltadas:

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

Como con los componentes anteriores, inicializas la navegación del enrutador React con el gancho useNavigate y luego utilizas el gancho useState para inicializar un estado de name, ingredients e instruction, cada uno con sus respectivas funciones de actualización. Estos son los campos que necesitarás para crear una receta válida.

A continuación, crea una función stripHtmlEntities que convertirá los caracteres especiales (como <) en sus valores escapados/codificados (como &lt;), respectivamente. Para hacer esto, agrega las líneas resaltadas al componente 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;

En la función stripHtmlEntities, reemplaza los caracteres < y > con sus valores escapados. De esta manera, no almacenarás HTML sin procesar en tu base de datos.

A continuación, agrega las líneas resaltadas para añadir las funciones onChange y onSubmit al componente NewRecipe para manejar la edición y envío del formulario:

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

La función onChange acepta la entrada del usuario evento y la función de establecimiento de estado, luego actualiza posteriormente el estado con el valor de entrada del usuario. En la función onSubmit, verificas que ninguno de los campos requeridos esté vacío. Luego construyes un objeto que contiene los parámetros necesarios para crear una nueva receta. Utilizando la función stripHtmlEntities, reemplazas los caracteres < y > en las instrucciones de la receta con su valor escapado y reemplazas cada salto de línea con una etiqueta de salto de línea, conservando así el formato de texto ingresado por el usuario. Por último, realizas una solicitud HTTP POST para crear la nueva receta y redirigir a su página en caso de una respuesta exitosa.

Para protegerse contra los ataques de Falsificación de Petición en Sitio Cruzado (CSRF), Rails adjunta un token de seguridad CSRF al documento HTML. Este token es necesario cada vez que se realiza una solicitud que no sea GET. Con la constante token en el código anterior, tu aplicación verifica el token en el servidor y genera una excepción si el token de seguridad no coincide con lo esperado. En la función onSubmit, la aplicación recupera el token CSRF incrustado en tu documento HTML por Rails y luego realiza una solicitud HTTP con una cadena JSON. Si la receta se crea correctamente, la aplicación redirige al usuario a la página de la receta donde pueden ver su receta recién creada.

Por último, devuelve el marcado que renderiza un formulario para que el usuario ingrese los detalles de la receta que desea crear. Agrega las líneas destacadas:

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

El marcado devuelto incluye un formulario que contiene tres campos de entrada; uno para el nombre de la receta, otro para los ingredientes de la receta y otro para las instrucciones. Cada campo de entrada tiene un controlador de eventos `onChange` que llama a la función `onChange`. También se adjunta un controlador de eventos `onSubmit` al botón de enviar que llama a la función `onSubmit` que envía los datos del formulario.

Guarda y sale del archivo.

Para acceder a este componente en el navegador, actualiza tu archivo de ruta con su ruta:

  1. nano app/javascript/routes/index.jsx

Actualiza tu archivo de ruta para incluir estas líneas destacadas:

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

Con la ruta en su lugar, guarda y sale del archivo.

Reinicia tu servidor de desarrollo y visita http://localhost:3000 en tu navegador. Navega hasta la página de recetas y haz clic en el botón Crear Nueva Receta. Encontrarás una página con un formulario para agregar recetas a tu base de datos:

Ingresa los detalles de la receta requeridos y haz clic en el botón Crear Receta. La receta recién creada aparecerá en la página. Cuando estés listo, cierra el servidor.

En este paso, agregaste la capacidad de crear recetas a tu aplicación de recetas de comida. En el siguiente paso, agregarás la funcionalidad para eliminar recetas.

Paso 9 — Eliminación de Recetas

En esta sección, modificarás tu componente de Receta para incluir una opción para eliminar recetas. Cuando hagas clic en el botón de eliminar en la página de la receta, la aplicación enviará una solicitud para eliminar una receta de la base de datos.

Primero, abre tu archivo Recipe.jsx para editarlo:

  1. nano app/javascript/components/Recipe.jsx

En el componente Recipe, agrega una función deleteRecipe con las líneas resaltadas:

~/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="">
...

En la función deleteRecipe, obtienes el id de la receta a eliminar, luego construyes tu URL y obtienes el token CSRF. Después, realizas una solicitud DELETE al controlador Recipes para eliminar la receta. La aplicación redirige al usuario a la página de recetas si la receta se elimina correctamente.

Para ejecutar el código en la función deleteRecipe cada vez que se hace clic en el botón de eliminar, pásalo como el manejador de eventos de clic al botón. Agrega un evento onClick al elemento del botón de eliminar en el componente:

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

En este punto del tutorial, tu archivo Recipe.jsx completo debería coincidir con este archivo:

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

Guarda y cierra el archivo.

Reinicia el servidor de la aplicación y navega hasta la página de inicio. Haz clic en el botón Ver Recetas para acceder a todas las recetas existentes, luego abre cualquier receta en particular y haz clic en el botón Eliminar Receta en la página para eliminar el artículo. Serás redirigido a la página de recetas y la receta eliminada ya no existirá.

¡Con el botón de eliminar funcionando, ahora tienes una aplicación de recetas completamente funcional!

Conclusión

En este tutorial, creaste una aplicación de recetas de comida con Ruby on Rails y un frontend de React, utilizando PostgreSQL como tu base de datos y Bootstrap para el estilo. Si deseas seguir construyendo con Ruby on Rails, considera seguir nuestro tutorial Cómo asegurar comunicaciones en una aplicación Rails de tres capas utilizando túneles SSH o visita nuestra serie Cómo programar en Ruby para refrescar tus habilidades en Ruby. Para profundizar en React, prueba Cómo mostrar datos de la API de DigitalOcean con React.

Source:
https://www.digitalocean.com/community/tutorials/how-to-set-up-a-ruby-on-rails-v7-project-with-a-react-frontend-on-ubuntu-20-04