Como Configurar um Projeto Ruby on Rails v7 com um Frontend React no Ubuntu 20.04

O autor selecionou a Electronic Frontier Foundation para receber uma doação como parte do programa Write for DOnations.

Introdução

O Ruby on Rails é um popular framework de aplicativos web no lado do servidor. Ele impulsiona muitas aplicações populares que existem na web hoje, como GitHub, Basecamp, SoundCloud, Airbnb e Twitch. Com ênfase na experiência do programador e na comunidade apaixonada construída ao seu redor, o Ruby on Rails fornecerá as ferramentas necessárias para construir e manter sua aplicação web moderna.

O React é uma biblioteca JavaScript usada para criar interfaces de usuário no front-end. Apoiado pelo Facebook, é uma das bibliotecas front-end mais populares usadas na web hoje. O React oferece recursos como um modelo de objeto de documento virtual (DOM), arquitetura de componentes e gerenciamento de estado, que tornam o processo de desenvolvimento front-end mais organizado e eficiente.

Com o frontend da web movendo-se em direção a frameworks separados do código do lado do servidor, combinar a elegância do Rails com a eficiência do React permitirá que você construa aplicativos poderosos e modernos informados pelas tendências atuais. Ao usar o React para renderizar componentes de dentro de uma visualização do Rails (em vez do motor de templates do Rails), seu aplicativo se beneficiará dos últimos avanços em JavaScript e desenvolvimento frontend, enquanto aproveita a expressividade do Ruby on Rails.

Neste tutorial, você criará uma aplicação Ruby on Rails que armazena suas receitas favoritas e depois as exibe com um frontend React. Quando terminar, você poderá criar, visualizar e excluir receitas usando uma interface React estilizada com Bootstrap:

Pré-requisitos

Para seguir este tutorial, você precisa:

Observação: A versão 7 do Rails não é compatível com versões anteriores. Se estiver usando a versão 5 do Rails, por favor, visite o tutorial Como Configurar um Projeto Ruby on Rails v5 com um Frontend React no Ubuntu 18.04.

Passo 1 — Criando uma Nova Aplicação Rails

Você vai construir sua aplicação de receitas no framework de aplicação Rails nesta etapa. Primeiro, você irá criar uma nova aplicação Rails, que será configurada para funcionar com React.

O Rails fornece vários scripts chamados geradores que criam tudo o que é necessário para construir uma aplicação web moderna. Para revisar uma lista completa desses comandos e o que eles fazem, execute o seguinte comando em seu terminal:

  1. rails -h

Este comando irá gerar uma lista abrangente de opções, permitindo que você defina os parâmetros da sua aplicação. Um dos comandos listados é o comando new, que cria uma nova aplicação Rails.

Agora, você irá criar uma nova aplicação Rails usando o gerador new. Execute o seguinte comando no seu terminal:

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

O comando anterior cria uma nova aplicação Rails em um diretório chamado rails_react_recipe, instala as dependências Ruby e JavaScript necessárias, e configura o Webpack. As flags associadas a este comando do gerador new incluem o seguinte:

  • A flag -d especifica o motor de banco de dados preferido, que neste caso é o PostgreSQL.
  • A flag -j especifica a abordagem JavaScript da aplicação. O Rails oferece algumas maneiras diferentes de lidar com o código JavaScript em aplicações Rails. A opção esbuild passada para a flag -j instrui o Rails a pré-configurar esbuild como o empacotador JavaScript preferido.
  • A flag -c especifica o processador de CSS da aplicação. Bootstrap é a opção preferida neste caso.
  • A flag -T instrui o Rails a pular a geração de arquivos de teste, já que você não estará escrevendo testes para este tutorial. Este comando também é sugerido se você deseja usar uma ferramenta de teste Ruby diferente daquela que o Rails fornece.

Assim que o comando terminar, vá para o diretório rails_react_recipe, que é o diretório raiz da sua aplicação:

  1. cd rails_react_recipe

Em seguida, liste o conteúdo do diretório:

  1. ls

Os conteúdos serão impressos de forma semelhante a isto:

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 diretório raiz possui vários arquivos e pastas gerados automaticamente que compõem a estrutura de uma aplicação Rails, incluindo um arquivo package.json contendo dependências para uma aplicação React.

Agora que você criou com sucesso uma nova aplicação Rails, você irá conectá-la a um banco de dados no próximo passo.

Passo 2 — Configurando o Banco de Dados

Antes de executar sua nova aplicação Rails, é necessário conectá-la a um banco de dados. Neste passo, você irá conectar a aplicação Rails recém-criada a um banco de dados PostgreSQL para que os dados da receita possam ser armazenados e recuperados conforme necessário.

O arquivo database.yml encontrado em config/database.yml contém detalhes do banco de dados, como nomes de banco de dados para diferentes ambientes de desenvolvimento. O Rails especifica um nome de banco de dados para os vários ambientes de desenvolvimento, acrescentando um sublinhado (_) seguido pelo nome do ambiente. Neste tutorial, você usará os valores padrão de configuração do banco de dados, mas pode alterar esses valores de configuração, se necessário.

Nota: Neste ponto, você pode alterar config/database.yml para definir qual papel do PostgreSQL você gostaria que o Rails usasse para criar seu banco de dados. Durante os pré-requisitos, você criou um papel que está protegido por uma senha no tutorial Como Usar o PostgreSQL com Sua Aplicação Ruby on Rails. Se você ainda não definiu o usuário, agora pode seguir as instruções para o Passo 4 — Configurando e Criando Seu Banco de Dados no mesmo tutorial de pré-requisitos.

O Rails oferece muitos comandos que facilitam o desenvolvimento de aplicações web, incluindo comandos para trabalhar com bancos de dados como create, drop e reset. Para criar um banco de dados para sua aplicação, execute o seguinte comando em seu terminal:

  1. rails db:create

Este comando cria um banco de dados development e test, produzindo a seguinte saída:

Output
Created database 'rails_react_recipe_development' Created database 'rails_react_recipe_test'

Agora que a aplicação está conectada a um banco de dados, inicie a aplicação executando o seguinte comando:

  1. bin/dev

O Rails fornece um script alternativo bin/dev que inicia uma aplicação Rails executando os comandos no arquivo Procfile.dev no diretório raiz do aplicativo usando a gema Foreman.

Assim que você executar este comando, seu prompt de comando desaparecerá e a seguinte saída será exibida em seu lugar:

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 acessar sua aplicação, abra uma janela do navegador e navegue até http://localhost:3000. A página de boas-vindas padrão do Rails será carregada, o que significa que você configurou corretamente sua aplicação Rails:

Para parar o servidor web, pressione CTRL+C no terminal onde o servidor está em execução. Você receberá uma mensagem de despedida do 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

Seu prompt do terminal então reaparecerá.

Você configurou com sucesso um banco de dados para sua aplicação de receitas de comida. No próximo passo, você irá instalar as dependências do JavaScript necessárias para montar sua interface de usuário React.

Passo 3 — Instalando Dependências de Frontend

Neste passo, você irá instalar as dependências do JavaScript necessárias no frontend de sua aplicação de receitas de comida. Elas incluem:

  • React para construir interfaces de usuário.
  • React DOM para permitir que o React interaja com o DOM do navegador.
  • React Router para lidar com a navegação em uma aplicação React.

Execute o seguinte comando para instalar esses pacotes com o gerenciador de pacotes Yarn:

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

Este comando utiliza o Yarn para instalar os pacotes especificados e adicioná-los ao arquivo package.json. Para verificar isso, abra o arquivo package.json localizado no diretório raiz do projeto:

  1. nano package.json

Os pacotes instalados serão listados sob a chave 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"
  }
}

Feche o arquivo pressionando CTRL+X.

Você instalou algumas dependências front-end para sua aplicação. Em seguida, você irá configurar uma página inicial para sua aplicação de receitas culinárias.

Passo 4 — Configurando a Página Inicial

Com as dependências necessárias instaladas, agora você criará uma página inicial para a aplicação servir como página inicial quando os usuários visitarem a aplicação pela primeira vez.

O Rails segue o padrão arquitetônico Model-View-Controller para aplicações. No padrão MVC, o objetivo de um controlador é receber solicitações específicas e encaminhá-las para o modelo ou visualização apropriados. Atualmente, a aplicação exibe a página de boas-vindas do Rails quando a URL raiz é carregada no navegador. Para alterar isso, você criará um controlador e uma visualização para a página inicial e, em seguida, a associará a uma rota.

O Rails fornece um gerador de controller para criar um controller. O gerador de controller recebe um nome de controller e uma ação correspondente. Para mais informações sobre isso, você pode revisar a documentação do Rails.

Este tutorial chamará o controller de Página Inicial. Execute o seguinte comando para criar um controller Página Inicial com uma ação index:

  1. rails g controller Homepage index

Nota:
No Linux, o erro FATAL: Listen error: unable to monitor directories for changes. pode resultar de um limite do sistema no número de arquivos que sua máquina pode monitorar para alterações. Execute o seguinte comando para corrigir isso:

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

Este comando aumentará permanentemente o número de diretórios que você pode monitorar com Listen para 524288. Você pode alterar isso novamente executando o mesmo comando e substituindo 524288 pelo número desejado.

A execução do comando controller gera os seguintes arquivos:

  • 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.
  • Um arquivo index.html.erb como a página de visualização para renderizar qualquer coisa relacionada à página inicial.

Além dessas novas páginas criadas pela execução do comando Rails, o Rails também atualiza o arquivo de rotas localizado em config/routes.rb, adicionando uma rota get para sua página inicial, que você modificará como sua rota raiz.

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

Neste arquivo, substitua get 'homepage/index' por root 'homepage#index' para que o arquivo corresponda ao seguinte:

~/rails_react_recipe/config/routes.rb
Rails.application.routes.draw do
  root 'homepage#index'
  # Para obter detalhes sobre o DSL disponível neste arquivo, consulte http://guides.rubyonrails.org/routing.html
end

Esta modificação instrui o Rails a mapear solicitações para a raiz da aplicação para a ação index do controlador Homepage, que por sua vez renderiza no navegador o que estiver no arquivo index.html.erb localizado em app/views/homepage/index.html.erb.

Salve e feche o arquivo.

Para verificar se isso está funcionando, inicie sua aplicação:

  1. bin/dev

Ao abrir ou atualizar a aplicação no navegador, uma nova página inicial para sua aplicação será carregada:

Depois de verificar se sua aplicação está funcionando, pressione CTRL+C para parar o servidor.

Em seguida, abra o arquivo ~/rails_react_recipe/app/views/homepage/index.html.erb:

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

Remova o código dentro do arquivo e, em seguida, salve-o como vazio. Fazendo isso, você garante que o conteúdo de index.html.erb não interfira na renderização do seu frontend em React.

Agora que você configurou a página inicial para sua aplicação, pode prosseguir para a próxima seção, onde configurará o frontend da sua aplicação para usar React.

Passo 5 — Configurando o React como o Frontend do seu Rails

Neste passo, você configurará o Rails para usar o React no frontend da aplicação, em vez do seu mecanismo de modelo. Esta nova configuração permitirá que você crie uma página inicial mais visualmente atraente com React.

Com a ajuda da opção esbuild especificada ao gerar a aplicação Rails, a maioria das configurações necessárias para permitir que o JavaScript funcione perfeitamente com o Rails já está em vigor. Tudo o que resta é carregar o ponto de entrada da aplicação React no ponto de entrada do esbuild para arquivos JavaScript. Para fazer isso, comece criando um diretório de componentes no diretório app/javascript:

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

O diretório components abrigará o componente para a página inicial, juntamente com outros componentes React na aplicação, incluindo o arquivo de entrada na aplicação React.

Em seguida, abra o arquivo application.js localizado em app/javascript/application.js:

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

Adicione a linha de código destacada ao arquivo:

~/rails_react_recipe/app/javascript/application.js
// Ponto de entrada para o script de construção no seu package.json
import "@hotwired/turbo-rails"
import "./controllers"
import * as bootstrap from "bootstrap"
import "./components"

A linha de código adicionada ao arquivo application.js importará o código no arquivo de entrada index.jsx, tornando-o disponível para esbuild para empacotamento. Com o diretório /components importado no ponto de entrada JavaScript da aplicação Rails, você pode criar um componente React para sua página inicial. A página inicial conterá alguns textos e um botão de chamada para ação para visualizar todas as receitas.

Salve e feche o arquivo.

Em seguida, crie um arquivo Home.jsx no diretório components:

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

Adicione o seguinte código ao arquivo:

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

Neste código, você importa o React e o componente Link do React Router. O componente Link cria um hiperlink para navegar de uma página para outra. Em seguida, você cria e exporta um componente funcional contendo alguma linguagem de marcação para a sua página inicial, estilizada com classes Bootstrap.

Salve e feche o arquivo.

Com o seu componente Home configurado, você irá agora configurar o roteamento usando o React Router. Crie um diretório routes no diretório app/javascript:

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

O diretório routes conterá algumas rotas com seus respectivos componentes. Sempre que uma rota específica for carregada, ela renderizará seu componente correspondente no navegador.

No diretório routes, crie um arquivo index.jsx:

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

Adicione o seguinte código a ele:

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

Neste arquivo de rota index.jsx, você importa os seguintes módulos: o módulo React que permite o uso do React, bem como os módulos BrowserRouter, Routes e Route do React Router, que ajudam na navegação de uma rota para outra. Por último, você importa o seu componente Home, que será renderizado sempre que uma solicitação corresponder à rota raiz (/). Quando quiser adicionar mais páginas à sua aplicação, você pode declarar uma rota neste arquivo e associá-la ao componente que deseja renderizar para essa página.

Salve e saia do arquivo.

Agora você configurou o roteamento usando o React Router. Para o React reconhecer as rotas disponíveis e utilizá-las, as rotas devem estar disponíveis no ponto de entrada da aplicação. Para conseguir isso, você vai renderizar suas rotas em um componente que o React vai renderizar no seu arquivo de entrada.

Crie um arquivo App.jsx no diretório app/javascript/components:

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

Adicione o seguinte código ao arquivo App.jsx:

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

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

No arquivo App.jsx, você importa o React e os arquivos de rota que acabou de criar. Então, exporta um componente para renderizar as rotas dentro de fragmentos. Esse componente será renderizado no ponto de entrada da aplicação, tornando as rotas disponíveis sempre que a aplicação for carregada.

Salve e feche o arquivo.

Agora que você configurou o seu App.jsx, pode renderizá-lo no seu arquivo de entrada. Crie um arquivo index.jsx no diretório components:

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

Adicione o seguinte código ao arquivo 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 />);
});

Nas linhas de import, você importa a biblioteca React, a função createRoot do ReactDOM e seu componente App. Usando a função createRoot do ReactDOM, você cria um elemento raiz como um elemento div anexado à página e renderiza seu componente App nele. Quando a aplicação for carregada, o React renderizará o conteúdo do componente App dentro do elemento div na página.

Salve e saia do arquivo.

Por fim, você vai adicionar alguns estilos CSS à sua página inicial.

Abra o arquivo application.bootstrap.scss no diretório ~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss:

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

Em seguida, substitua o conteúdo do arquivo application.bootstrap.scss pelo seguinte 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;
}

Você definiu algumas cores personalizadas para a página. A seção .hero criará o esqueleto para uma imagem de destaque ou um grande banner na página inicial do seu site, que você adicionará posteriormente. Além disso, o estilo custom-button.btn formata o botão que o usuário usará para entrar na aplicação.

Com seus estilos CSS no lugar, salve e saia do arquivo.

Em seguida, reinicie o servidor web da sua aplicação:

  1. bin/dev

Depois, recarregue a aplicação no seu navegador. Uma nova página inicial será carregada:

Pare o servidor web com CTRL+C.

Você configurou sua aplicação para usar o React como frontend nesta etapa. Na próxima etapa, você criará modelos e controladores que permitirão criar, ler, atualizar e excluir receitas.

Etapa 6 — Criando o Controlador e Modelo de Receitas

Agora que você configurou um frontend React para sua aplicação, você criará um modelo e controlador de Receita. O modelo de receita representará a tabela do banco de dados contendo informações sobre as receitas do usuário, enquanto o controlador receberá e lidará com solicitações para criar, ler, atualizar ou excluir receitas. Quando um usuário solicita uma receita, o controlador de receitas recebe essa solicitação e a passa para o modelo de receita, que recupera os dados solicitados do banco de dados. O modelo então retorna os dados da receita como resposta para o controlador. Finalmente, essas informações são exibidas no navegador.

Comece criando um modelo `Recipe` usando o subcomando `generate model` fornecido pelo Rails e especificando o nome do modelo junto com suas colunas e tipos de dados. Execute o seguinte comando:

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

O comando anterior instrui o Rails a criar um modelo `Recipe` juntamente com uma coluna `name` do tipo `string`, colunas `ingredients` e `instruction` do tipo `text`, e uma coluna `image` do tipo `string`. Este tutorial nomeou o modelo `Recipe`, porque os modelos no Rails usam um nome singular enquanto suas tabelas de banco de dados correspondentes usam um nome plural.

Executar o comando `generate model` cria dois arquivos e imprime a seguinte saída:

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

Os dois arquivos criados são:

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

Em seguida, você editará o arquivo do modelo de receita para garantir que apenas dados válidos sejam salvos no banco de dados. Você pode conseguir isso adicionando algumas validações de banco de dados ao seu modelo.

Abra o modelo de receita localizado em `app/models/recipe.rb`:

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

Adicione as seguintes linhas de código destacadas ao arquivo:

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

Neste código, você adiciona a validação do modelo, que verifica a presença dos campos name, ingredients e instruction. Sem esses três campos, uma receita é inválida e não será salva no banco de dados.

Salve e feche o arquivo.

Para o Rails criar a tabela recipes no seu banco de dados, você precisa executar uma migração, que é uma maneira de fazer alterações no seu banco de dados programaticamente. Para garantir que a migração funcione com o banco de dados que você configurou, é necessário fazer alterações no arquivo 20221017220817_create_recipes.rb.

Abra este arquivo no seu editor:

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

Adicione os materiais destacados para que seu arquivo corresponda ao seguinte:

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 arquivo de migração contém uma classe Ruby com um método change e um comando para criar uma tabela chamada recipes juntamente com as colunas e seus tipos de dados. Você também atualiza 20221017220817_create_recipes.rb com uma restrição NOT NULL nas colunas name, ingredients e instruction adicionando null: false, garantindo que essas colunas tenham um valor antes de alterar o banco de dados. Finalmente, você adiciona uma URL de imagem padrão para sua coluna de imagem; esta pode ser outra URL se desejar usar uma imagem diferente.

Com essas alterações, salve e saia do arquivo. Agora você está pronto para executar sua migração e criar sua tabela. No terminal, execute o seguinte comando:

  1. rails db:migrate

Você usa o comando de migração de banco de dados para executar as instruções no seu arquivo de migração. Uma vez que o comando seja executado com sucesso, você receberá uma saída semelhante à seguinte:

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

Com seu modelo de receitas no lugar, você irá criar o controlador de receitas para adicionar a lógica de criação, leitura e exclusão de receitas. Execute o seguinte comando:

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

Neste comando, você cria um controlador Recipes em um diretório api/v1 com ações index, create, show e destroy. A ação index lidará com a obtenção de todas as suas receitas; a ação create será responsável por criar novas receitas; a ação show buscará uma única receita, e a ação destroy terá a lógica para excluir uma receita.

Você também passa algumas flags para tornar o controlador mais leve, incluindo:

  • --skip-template-engine, que instrui o Rails a pular a geração de arquivos de visualização do Rails, já que o React cuida das suas necessidades de front-end.
  • --no-helper, que instrui o Rails a pular a geração de um arquivo de helper para o seu controlador.

A execução do comando também atualiza o arquivo de rotas com uma rota para cada ação no controlador Recipes.

Quando o comando é executado, ele imprimirá uma saída 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 essas rotas, você fará alterações no seu arquivo config/routes.rb. Abra o arquivo routes.rb no seu editor de texto:

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

Atualize este arquivo para se parecer com o seguinte código, alterando ou adicionando as linhas destacadas:

~/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'
  # Defina as rotas da sua aplicação conforme o DSL em https://guides.rubyonrails.org/routing.html

  # Define a rota do caminho raiz ("/")
  # root "articles#index"
end

No arquivo de rotas, modifique o verbo HTTP das rotas create e destroy para que possam post e delete dados. Modifique também as rotas para as ações show e destroy adicionando um parâmetro :id à rota. :id irá conter o número de identificação da receita que você deseja ler ou excluir.

Adicione uma rota catch-all com get '/*path' que direcionará qualquer outra solicitação que não corresponda às rotas existentes para a ação index do controlador homepage. O roteamento no front-end lidará com solicitações não relacionadas à criação, leitura ou exclusão de receitas.

Salve e saia do arquivo.

Para avaliar a lista de rotas disponíveis em sua aplicação, execute o seguinte comando:

  1. rails routes

A execução deste comando exibe uma extensa lista de padrões de URI, verbos e controladores ou ações correspondentes para o seu projeto.

Em seguida, você adicionará a lógica para obter todas as receitas de uma vez. O Rails utiliza a biblioteca ActiveRecord para lidar com tarefas relacionadas ao banco de dados, como esta. O ActiveRecord conecta classes a tabelas de banco de dados relacionais e fornece uma API rica para trabalhar com elas.

Para obter todas as receitas, você utilizará o ActiveRecord para consultar a tabela de receitas e buscar todas as receitas no banco de dados.

Abra o arquivo recipes_controller.rb com o seguinte comando:

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

Adicione as linhas destacadas ao controlador de receitas:

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

Na sua ação index, você utiliza o método all do ActiveRecord para obter todas as receitas no seu banco de dados. Utilizando o método order, você as ordena em ordem decrescente pela data de criação, o que colocará as receitas mais recentes primeiro. Por fim, você envia a lista de receitas como uma resposta JSON com render.

Em seguida, você adicionará a lógica para criar novas receitas. Assim como ao buscar todas as receitas, você dependerá do ActiveRecord para validar e salvar os detalhes da receita fornecidos. Atualize o seu controlador de receitas com as seguintes linhas de código destacadas:

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

No método create, você utiliza o método create do ActiveRecord para criar uma nova receita. O método create pode atribuir todos os parâmetros do controlador ao modelo de uma vez. Esse método facilita a criação de registros, mas abre a possibilidade de uso malicioso. O uso malicioso pode ser evitado ao usar a funcionalidade parâmetros fortes fornecida pelo Rails. Dessa forma, os parâmetros não podem ser atribuídos a menos que tenham sido permitidos. Você passa um parâmetro recipe_params para o método create em seu código. O recipe_params é um método private onde você permite que os parâmetros do controlador evitem que conteúdo incorreto ou malicioso entre no seu banco de dados. Neste caso, você permite um parâmetro name, image, ingredients e instruction para o uso válido do método create.

Seu controlador de receitas agora pode ler e criar receitas. Tudo o que resta é a lógica para ler e excluir uma única receita. Atualize seu controlador de receitas com o código destacado:

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

Nas novas linhas de código, você cria um método set_recipe privado chamado por um before_action apenas quando as ações show e delete correspondem a uma solicitação. O método set_recipe utiliza o método find do ActiveRecord para encontrar uma receita cujo id corresponde ao id fornecido nos params e atribui a uma variável de instância @recipe. Na ação show, você retorna o objeto @recipe definido pelo método set_recipe como uma resposta JSON.

Na ação destroy, você fez algo semelhante usando o operador de navegação segura do Ruby &., que evita erros nil ao chamar um método. Essa adição permite que você exclua uma receita apenas se ela existir e, em seguida, envie uma mensagem como resposta.

Depois de fazer essas alterações no recipes_controller.rb, salve e feche o arquivo.

Neste passo, você criou um modelo e um controlador para suas receitas. Você escreveu toda a lógica necessária para trabalhar com receitas no backend. Na próxima seção, você criará componentes para visualizar suas receitas.

Passo 7 — Visualizando Receitas

Nesta seção, você irá criar componentes para visualizar receitas. Você criará duas páginas: uma para visualizar todas as receitas existentes e outra para visualizar receitas individuais.

Você começará criando uma página para visualizar todas as receitas. Antes de criar a página, você precisa de receitas para trabalhar, já que seu banco de dados está vazio no momento. O Rails oferece uma maneira de criar dados iniciais para a sua aplicação.

Abra o arquivo de semente chamado seeds.rb para edição:

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

Substitua o conteúdo inicial do arquivo de semente com o seguinte 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

Neste código, você usa um loop que instrui o Rails a criar nove receitas com seções para nome, ingredientes e instrução. Salve e saia do arquivo.

Para popular o banco de dados com esses dados, execute o seguinte comando no seu terminal:

  1. rails db:seed

Executando este comando, serão adicionadas nove receitas ao seu banco de dados. Agora você pode buscá-las e renderizá-las no frontend.

O componente para visualizar todas as receitas fará uma requisição HTTP para a ação index no RecipesController para obter uma lista de todas as receitas. Essas receitas serão então exibidas em cartões na página.

Crie um arquivo Recipes.jsx no diretório app/javascript/components:

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

Assim que o arquivo estiver aberto, importe os módulos React, useState, useEffect, Link e useNavigate adicionando as seguintes linhas:

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

Em seguida, adicione as linhas destacadas para criar e exportar um componente funcional React chamado 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 do componente Recipe, a API de navegação do React Router irá chamar o hook useNavigate. O hook useState do React irá inicializar o estado recipes, que é um array vazio ([]), e uma função setRecipes para atualizar o estado recipes.

Em seguida, em um hook useEffect, você fará uma requisição HTTP para buscar todas as suas receitas. Para fazer isso, adicione as linhas destacadas:

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

No seu hook useEffect, você faz uma chamada HTTP para buscar todas as receitas usando a Fetch API. Se a resposta for bem-sucedida, a aplicação salva o array de receitas no estado recipes. Se ocorrer um erro, ela redirecionará o usuário para a página inicial.

Por fim, retorne a marcação dos elementos que serão avaliados e exibidos na página do navegador quando o componente for renderizado. Neste caso, o componente irá renderizar um card de receitas a partir do estado recipes. Adicione as linhas destacadas em 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;

Salve e saia de Recipes.jsx.

Agora que você criou um componente para exibir todas as receitas, você irá criar uma rota para ele. Abra o arquivo de rota do front-end app/javascript/routes/index.jsx:

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

Adicione as linhas destacadas no arquivo:

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

Salve e saia do arquivo.

Neste ponto, é uma boa ideia verificar se o seu código está funcionando conforme o esperado. Como feito anteriormente, use o seguinte comando para iniciar o seu servidor:

  1. bin/dev

Em seguida, abra o aplicativo em seu navegador. Pressione o botão Ver Receita na página inicial para acessar uma página de exibição com suas receitas:

Utilize CTRL+C no seu terminal para parar o servidor e retornar ao prompt.

Agora que você pode visualizar todas as receitas em seu aplicativo, é hora de criar um segundo componente para visualizar receitas individuais. Crie um arquivo Recipe.jsx no diretório app/javascript/components:

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

Assim como o componente Recipes, importe os módulos React, useState, useEffect, Link, useNavigate, e useParam adicionando as seguintes linhas:

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

Em seguida, adicione as linhas destacadas para criar e exportar um componente funcional React chamado 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;

Assim como o componente Recipes, você inicializa a navegação do React Router com o gancho useNavigate. Um estado recipe e uma função setRecipe irão atualizar o estado com o gancho useState. Além disso, você chama o gancho useParams, que retorna um objeto cujos pares chave/valor são parâmetros de URL.

Para encontrar uma receita específica, seu aplicativo precisa saber o id da receita, o que significa que o seu componente Recipe espera um param id na URL. Você pode acessar isso por meio do objeto params que contém o valor de retorno do gancho useParams.

Em seguida, declare um gancho useEffect onde você acessará o id param do objeto params. Assim que você obtiver o id do parâmetro da receita, você fará uma solicitação HTTP para buscar a receita. Adicione as linhas destacadas ao seu arquivo:

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

No gancho useEffect, você usa o valor params.id para fazer uma solicitação HTTP GET para buscar a receita que possui o id e então salvá-la no estado do componente usando a função setRecipe. O aplicativo redireciona o usuário para a página de receitas se a receita não existir.

Em seguida, adicione uma função addHtmlEntities, que será usada para substituir entidades de caracteres por entidades HTML no componente. A função addHtmlEntities receberá uma string e substituirá todos os colchetes de abertura e fechamento escapados por suas entidades HTML. Esta função ajudará você a converter qualquer caractere escapado que tenha sido salvo nas instruções da sua receita. Adicione as linhas destacadas:

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

Por fim, retorne a marcação para renderizar a receita no estado do componente na página, adicionando as linhas destacadas:

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

Com uma função ingredientList, você divide seus ingredientes de receita separados por vírgulas em uma matriz e mapeia sobre eles para criar uma lista de ingredientes. Se não houver ingredientes, o aplicativo exibe uma mensagem que diz Nenhum ingrediente disponível. Você também substitui todos os colchetes de abertura e fechamento nas instruções da receita ao passá-los pela função addHtmlEntities. Por fim, o código exibe a imagem da receita como uma imagem principal, adiciona um botão Excluir Receita ao lado da instrução da receita e adiciona um botão que retorna à página de receitas.

Observação: Usar o atributo dangerouslySetInnerHTML do React é arriscado, pois expõe seu aplicativo a ataques de cross-site scripting. Esse risco é reduzido garantindo que caracteres especiais inseridos ao criar receitas sejam substituídos usando a função stripHtmlEntities declarada no componente NewRecipe.

Salve e saia do arquivo.

Para visualizar o componente Recipe em uma página, você o adicionará ao seu arquivo de rotas. Abra seu arquivo de rotas para edição:

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

Adicione as seguintes linhas destacadas ao arquivo:

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

Você importa seu componente Recipe neste arquivo de rota e adiciona uma rota. Sua rota possui um param :id que será substituído pelo id da receita que você deseja visualizar.

Salve e feche o arquivo.

Utilize o script bin/dev para iniciar seu servidor novamente e, em seguida, visite http://localhost:3000 em seu navegador. Clique no botão Ver Receitas para navegar até a página de receitas. Na página de receitas, acesse qualquer receita clicando no botão Ver Receita. Você será recebido com uma página preenchida com os dados do seu banco de dados:

Você pode parar o servidor com CTRL+C.

Neste passo, você adicionou nove receitas ao seu banco de dados e criou componentes para visualizar essas receitas, tanto individualmente quanto em coleção. No próximo passo, você adicionará um componente para criar receitas.

Passo 8 — Criando Receitas

O próximo passo para ter um aplicativo de receitas de alimentos utilizável é a capacidade de criar novas receitas. Neste passo, você criará um componente para essa funcionalidade. O componente conterá um formulário para coletar os detalhes da receita necessários do usuário e, em seguida, fará uma solicitação para a ação create no controlador Recipe para salvar os dados da receita.

Crie um arquivo NewRecipe.jsx no diretório app/javascript/components:

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

No novo arquivo, importe os módulos React, useState, Link e useNavigate que você usou em outros componentes:

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

Em seguida, crie e exporte um componente funcional NewRecipe adicionando as linhas 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("");
};

export default NewRecipe;

Como nos componentes anteriores, você inicializa a navegação do React router com o gancho useNavigate e, em seguida, utiliza o gancho useState para inicializar os estados name, ingredients e instruction, cada um com suas respectivas funções de atualização. Esses são os campos necessários para criar uma receita válida.

Em seguida, crie uma função stripHtmlEntities que converterá caracteres especiais (como <) em seus valores escapados/encodificados (como &lt;), respectivamente. Para fazer isso, adicione as linhas destacadas ao 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;

Na função stripHtmlEntities, substitua os caracteres < e > por seus valores escapados. Dessa forma, você não armazenará HTML puro no seu banco de dados.

Em seguida, adicione as linhas destacadas para incluir as funções onChange e onSubmit ao componente NewRecipe para lidar com a edição e o envio do formulário:

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

O código da função onChange aceita a entrada do usuário event e a função do definidor de estado. Em seguida, ele atualiza o estado com o valor de entrada do usuário. Na função onSubmit, verifica-se que nenhum dos inputs obrigatórios está vazio. Em seguida, é construído um objeto contendo os parâmetros necessários para criar uma nova receita. Utilizando a função stripHtmlEntities, substituímos os caracteres < e > na instrução da receita pelo seu valor escapado e substituímos cada caractere de nova linha por uma quebra de linha, mantendo assim o formato de texto inserido pelo usuário. Por fim, é feita uma solicitação HTTP POST para criar a nova receita e redirecionar para sua página em caso de uma resposta bem-sucedida.

Para se proteger contra ataques de falsificação de solicitação entre sites (CSRF), o Rails anexa um token de segurança CSRF ao documento HTML. Esse token é necessário sempre que uma solicitação não-GET é feita. Com a constante token no código anterior, sua aplicação verifica o token no servidor e lança uma exceção se o token de segurança não corresponder ao esperado. Na função onSubmit, a aplicação recupera o token CSRF incorporado em seu documento HTML pelo Rails e então faz uma solicitação HTTP com uma string JSON. Se a receita for criada com sucesso, a aplicação redireciona o usuário para a página da receita, onde eles podem ver a receita recém-criada.

Por fim, retorne a marcação que renderiza um formulário para o usuário inserir os detalhes da receita que desejam criar. Adicione as linhas 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;

A marcação retornada inclui um formulário que contém três campos de entrada; um para cada recipeName, recipeIngredients e instruction. Cada campo de entrada tem um manipulador de evento onChange que chama a função onChange. Um manipulador de evento onSubmit também é anexado ao botão de enviar e chama a função onSubmit que envia os dados do formulário.

Salve e saia do arquivo.

Para acessar este componente no navegador, atualize seu arquivo de rota com sua rota:

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

Atualize seu arquivo de rota para incluir estas linhas 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>
);

Com a rota no lugar, salve e saia do seu arquivo.

Reinicie seu servidor de desenvolvimento e visite http://localhost:3000 no seu navegador. Navegue até a página de receitas e clique no botão Criar Nova Receita. Você encontrará uma página com um formulário para adicionar receitas ao seu banco de dados:

Insira os detalhes da receita necessários e clique no botão Criar Receita. A receita recém-criada aparecerá então na página. Quando estiver pronto, feche o servidor.

Neste passo, você adicionou a capacidade de criar receitas à sua aplicação de receitas culinárias. No próximo passo, você adicionará a funcionalidade para deletar receitas.

Passo 9 — Deletando Receitas

Nesta seção, você irá modificar seu componente de Receita para incluir uma opção de exclusão de receitas. Ao clicar no botão de exclusão na página da receita, a aplicação enviará uma solicitação para deletar a receita do banco de dados.

Primeiro, abra seu arquivo Recipe.jsx para edição:

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

No componente Recipe, adicione uma função deleteRecipe com as linhas destacadas:

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

Na função deleteRecipe, você obtém o id da receita a ser excluída, em seguida, constrói sua URL e obtém o token CSRF. Em seguida, você faz uma solicitação DELETE para o controlador Recipes para excluir a receita. A aplicação redireciona o usuário para a página de receitas se a receita for excluída com sucesso.

Para executar o código na função deleteRecipe sempre que o botão de exclusão for clicado, passe-a como manipulador de evento de clique para o botão. Adicione um evento onClick ao elemento do botão de exclusão no 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>
  );
...

Neste ponto do tutorial, seu arquivo completo Recipe.jsx deve corresponder a este arquivo:

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

Salve e saia do arquivo.

Reinicie o servidor da aplicação e navegue até a página inicial. Clique no botão Ver Receitas para acessar todas as receitas existentes, em seguida, abra qualquer receita específica e clique no botão Excluir Receita na página para deletar a receita. Você será redirecionado para a página de receitas, e a receita excluída não existirá mais.

Com o botão de exclusão funcionando, agora você tem um aplicativo de receitas totalmente funcional!

Conclusão

Neste tutorial, você criou uma aplicação de receitas de comida com Ruby on Rails e uma interface React, utilizando PostgreSQL como seu banco de dados e Bootstrap para estilização. Se você deseja continuar construindo com Ruby on Rails, considere seguir nosso tutorial Como Segurar Comunicações em uma Aplicação Rails de Três Camadas Usando Túneis SSH ou visite nossa série Como Codificar em Ruby para atualizar suas habilidades em Ruby. Para se aprofundar no React, experimente o tutorial Como Exibir Dados da API da DigitalOcean com 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