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 aplicação web no lado do servidor. Ele alimenta muitas aplicações populares que existem na web hoje, como GitHub, Basecamp, SoundCloud, Airbnb e Twitch. Com seu foco 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.

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

Com a mudança do frontend da web em direção a frameworks separados do código 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 mecanismo de template do Rails), seu aplicativo se beneficiará dos avanços mais recentes em JavaScript e desenvolvimento frontend, ao mesmo tempo em que aproveita a expressividade do Ruby on Rails.

Neste tutorial, você criará uma aplicação Ruby on Rails que armazena suas receitas favoritas e as exibe com um frontend React. Ao finalizar, você será capaz de criar, visualizar e excluir receitas usando uma interface React estilizada com Bootstrap:

Pré-requisitos

Para seguir este tutorial, você precisa:

Nota: 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ê irá construir sua aplicação de receitas no framework de aplicativos Rails neste passo. 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 no seu terminal:

  1. rails -h

Este comando fornecerá 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 gerador new incluem o seguinte:

  • A flag -d especifica o mecanismo de banco de dados preferido, que neste caso é o PostgreSQL.
  • A flag -j especifica a abordagem de 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 o 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ê quiser 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, você deve 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 anexando um sublinhado (_) seguido pelo nome do ambiente. Neste tutorial, você usará os valores de configuração padrão do banco de dados, mas pode alterar seus valores de configuração se necessário.

Observação: 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 no 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 vá para 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. No próximo passo, você instalará as dependências JavaScript necessárias para montar seu frontend React.

Passo 3 — Instalando Dependências de Frontend

Neste passo, você instalará as dependências JavaScript necessárias no frontend de sua aplicação de receitas. 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 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 de front-end para sua aplicação. Em seguida, você irá configurar uma página inicial para a sua aplicação de receitas de comida.

Passo 4 — Configurando a Página Inicial

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

O Rails segue o padrão arquitetural Model-View-Controller para aplicações. No padrão MVC, o propósito de um controlador é receber solicitações específicas e encaminhá-las para o modelo ou visualização apropriados. A aplicação atualmente exibe a página de boas-vindas do Rails quando a URL raiz é carregada no navegador. Para mudar isso, você irá criar um controlador e visualização para a página inicial e então associá-lo a uma rota.

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

Este tutorial irá chamar o controlador de PáginaInicial. Execute o seguinte comando para criar um controlador PáginaInicial com uma ação index:

  1. rails g controller Homepage index

Nota:
No Linux, o erro FATAL: Erro de escuta: impossível monitorar diretórios para mudanças. pode resultar de um limite do sistema no número de arquivos que sua máquina pode monitorar para mudanças. Execute o seguinte comando para corrigi-lo:

  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 ao executar o comando Rails, o Rails também atualiza seu 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 detalhes sobre a DSL disponível dentro deste 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 PáginaInicial, 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 que 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. Ao fazer isso, você garante que o conteúdo de index.html.erb não interfira com a renderização do React no seu frontend.

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

Passo 5 — Configurando o React como Seu Frontend do Rails

Nesta etapa, você irá configurar o Rails para usar o React na interface do aplicativo, em vez de seu mecanismo de modelo. Essa nova configuração permitirá que você crie uma página inicial mais visualmente atraente com o React.

Com a ajuda da opção esbuild especificada ao gerar o aplicativo Rails, a maior parte da configuração necessária para permitir que o JavaScript funcione perfeitamente com o Rails já está em vigor. Tudo o que resta é carregar o ponto de entrada do aplicativo 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 no aplicativo, incluindo o arquivo de entrada no aplicativo 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 compilação em 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 o esbuild para empacotamento. Com o diretório /components importado no ponto de entrada JavaScript do aplicativo 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 hyperlink para navegar de uma página para outra. Em seguida, você cria e exporta um componente funcional contendo alguma linguagem de marcação para sua página inicial, estilizada com classes do Bootstrap.

Salve e feche o arquivo.

Com seu componente Home configurado, agora você irá 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 componentes correspondentes. Sempre que qualquer rota especificada for carregada, ela renderizará seu componente correspondente para o 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 usar o React, bem como os módulos BrowserRouter, Routes e Route do React Router, que juntos ajudam a navegar de uma rota para outra. Por fim, você importa seu componente Home, que será renderizado sempre que uma solicitação corresponder à rota raiz (/). Quando você quiser adicionar mais páginas à sua aplicação, pode declarar uma rota neste arquivo e combiná-la com o componente que deseja renderizar para essa página.

Salve e saia do arquivo.

Agora você configurou o roteamento usando o React Router. Para que o React esteja ciente das rotas disponíveis e as utilize, as rotas devem estar disponíveis no ponto de entrada da aplicação. Para conseguir isso, você irá renderizar suas rotas em um componente que o React 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. Em seguida, exporta um componente para renderizar as rotas dentro de fragmentos. Este 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 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 é 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ê 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 esboço para uma imagem de destaque, ou um grande banner da web 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 acessar a 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. No próximo passo, você criará modelos e controladores que permitirão criar, ler, atualizar e excluir receitas.

Passo 6 — Criando o Controlador e Modelo de Receitas

Agora que você configurou um frontend React para sua aplicação, você criará um modelo e um 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 receita 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 ao controlador. Finalmente, estas informações são exibidas no navegador.

Comece criando um modelo de Receita usando o subcomando generate model fornecido pelo Rails e especificando o nome do modelo juntamente 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 de Receita juntamente com uma coluna name do tipo string, uma coluna ingredients e instruction do tipo text, e uma coluna image do tipo string. Este tutorial nomeou o modelo de Receita, porque os modelos no Rails usam um nome singular enquanto suas tabelas de banco de dados correspondentes usam um nome plural.

A execução do 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ê irá editar o arquivo do modelo de receita para garantir que apenas dados válidos sejam salvos no banco de dados. Você pode fazer isso adicionando algumas validações de banco de dados ao seu modelo.

Abra seu 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 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 validação de 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, você deve 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. Por fim, você adiciona uma URL de imagem padrão para sua coluna de imagem; esta poderia ser outra URL se você quiser usar uma imagem diferente.

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

  1. rails db:migrate

Você utiliza 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 é 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 o seu modelo de receita 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 busca de todas as suas receitas; a ação create será responsável pela criação de novas receitas; a ação show buscará uma única receita, e a ação destroy conterá 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 suas rotas de aplicativo conforme o DSL em https://guides.rubyonrails.org/routing.html

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

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

Você adiciona uma rota de captura geral 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 de front-end lidará com solicitações não relacionadas à criação, leitura ou exclusão de receitas.

Salve e saia do arquivo.

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

  1. rails routes

Executar este comando exibe uma lista extensa 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 só vez. O Rails usa a biblioteca ActiveRecord para lidar com tarefas relacionadas a bancos de dados como essa. 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ê usará 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ê usa 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 último, 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 fornecida. Atualize o 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

Na ação 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 prevenido pelo uso da 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 no qual você permite os parâmetros do controlador para evitar 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.

Agora, seu controlador de receitas 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 corresponda ao id fornecido nos params e atribui-a 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 de 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.

Após fazer essas alterações no arquivo 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ê 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 sua aplicação.

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

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

Substitua o conteúdo inicial do arquivo de sementes pelo 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 alimentar o banco de dados com esses dados, execute o seguinte comando no seu terminal:

  1. rails db:seed

Executar este comando adiciona nove receitas ao seu banco de dados. Agora você pode buscá-las e exibi-las no frontend.

O componente para visualizar todas as receitas fará uma solicitaçã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, ele redirecionará o usuário para a página inicial.

Por fim, retorne a marcação para os 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 do estado recipes. Adicione as linhas destacadas ao arquivo 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 do arquivo Recipes.jsx.

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

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

Adicione as 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";

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

Salve e saia do arquivo.

Neste momento, é uma boa ideia verificar se o seu código está funcionando conforme o esperado. Como fez antes, use o seguinte comando para iniciar 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:

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

Agora que você pode visualizar todas as receitas em sua aplicação, é 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 hook useNavigate. Um estado recipe e uma função setRecipe atualizarão o estado com o hook useState. Além disso, você chama o hook useParams, que retorna um objeto cujos pares chave/valor são parâmetros de URL.

Para encontrar uma receita específica, sua aplicação precisa saber o id da receita, o que significa que seu componente Recipe espera um id param na URL. Você pode acessar isso através do objeto params que mantém o valor de retorno do hook useParams.

Próximo, declare um gancho useEffect onde você acessará o id param do objeto params. Assim que você obter o id param 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ê utiliza o valor params.id para fazer uma solicitação HTTP GET para buscar a receita que possui o id e depois 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. Essa função ajudará 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;

Finalmente, 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írgula em uma matriz e mapeia sobre ela 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 passando-as pela função addHtmlEntities. Por último, o código exibe a imagem da receita como uma imagem hero, adiciona um botão Excluir Receita ao lado das instruções da receita e adiciona um botão que volta para a 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 os 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 tem um param :id que será substituído pelo id da receita que você deseja visualizar.

Salve e feche o arquivo.

Use o script bin/dev para iniciar seu servidor novamente, 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 correspondente. Você será recebido com uma página populada 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 uma 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 alimentares utilizável é a capacidade de criar novas receitas. Neste passo, você criará um componente para esta 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;

Assim como nos componentes anteriores, você inicializa a navegação do React router com o gancho useNavigate e então usa o gancho useState para inicializar um estado name, ingredients e instruction, cada um com suas respectivas funções de atualização. Estes são os campos que você precisará criar para 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 > pelos seus valores escapados. Dessa forma, você não armazenará HTML bruto no seu banco de dados.

Em seguida, adicione as linhas destacadas para adicionar as funções onChange e onSubmit ao componente NewRecipe para lidar com a edição e 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;

A função onChange aceita a entrada do usuário event e a função de definição do estado, em seguida, ela atualiza o estado com o valor de entrada do usuário. Na função onSubmit, verifica-se que nenhum dos campos 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 quebra de linha por uma tag de quebra, 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 uma resposta bem-sucedida.

Para proteção contra ataques de falsificação de solicitação entre sites (CSRF), o Rails anexa um token de segurança CSRF ao documento HTML. Este 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 no 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 podem visualizar a nova receita criada.

Por fim, retorne a marcação que renderiza um formulário para que o usuário insira os detalhes da receita que deseja 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;

O marcado retornado inclui um formulário que contém três campos de entrada; um para cada um dos campos 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 está vinculado ao botão de envio 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 em 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 então aparecerá na página. Quando estiver pronto, feche o servidor.

Neste passo, você adicionou a capacidade de criar receitas ao seu aplicativo de receitas. No próximo passo, você adicionará a funcionalidade para excluir receitas.

Passo 9 — Excluindo Receitas

Nesta seção, você modificará seu componente de Receita para incluir uma opção para excluir receitas. Quando você clicar no botão de exclusão na página da receita, o aplicativo enviará uma solicitação para excluir uma 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. O aplicativo 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 Recipe.jsx completo 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 de aplicativos 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 excluir o artigo. 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, você agora tem um aplicativo de receitas totalmente funcional!

Conclusão

Neste tutorial, você criou um aplicativo de receitas de alimentos com Ruby on Rails e uma interface React, utilizando o PostgreSQL como seu banco de dados e o Bootstrap para estilização. Se desejar continuar desenvolvendo 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 Programar em Ruby para revisar 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