--People who want to create an app by separating the back end and front end (this time Rails and React)
I've been running away from the front end for a long time, but it's a memo when I started React because I thought it was a bad idea to learn something.
If anything, it will be written with an emphasis on "building an environment that separates the back end and front end", so there may be almost no explanation of the code itself.
I think it's better to read Rails as well as React and TypeScript after learning to some extent in advance.
We start by writing a Dockerfile, so if you follow the steps properly, you can make the same thing. For those who want to move their hands and get a feel for the atmosphere.
** Technology used **
rails-react-ts-sample
├─ backend
├─ app
├─ bin
├─ config
├─ db
├─ lib
├─ log
├─ public
├─ storage
├─ test
├─ tmp
├─ vendor
├─ Other files
├─ frontend
├─ react-app
├─ node_modules
├─ public
├─ src
├─ components
├─ TodoForm.tsx
├─ TodoItem.tsx
├─ TodoList.tsx
├─ Types.d.tsx
├─ App.tsx
├─ index.tsx
├─ Other files
├─ Other files
├─ docker-compose.yml
I think that a common pattern is to partially incorporate React into a Rails-based project using gems such as "react-rails", but this time the back end (Rails) and front end (React) ) Divided.
Rails will be created in API mode.
First of all, create various folders and files to build the environment. For those who are troublesome, we have prepared a sample code, so please clone it appropriately and use it. https://github.com/kazama1209/rails-react-ts-sample
$ mkdir rails-react-ts-sample
$ cd rails-react-ts-sample
This area can be any name you like.
$ touch docker-compose.yml
yml:./docker-compose.yml
version: "3"
services:
db:
image: mysql:5.7
command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci
environment:
MYSQL_ROOT_PASSWORD: password
volumes:
- mysql-data:/var/lib/mysql
- /tmp/dockerdir:/etc/mysql/conf.d/
ports:
- 3306:3306
api:
build:
context: ./backend/
dockerfile: Dockerfile
command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
volumes:
- ./backend:/myapp
- ./backend/vendor/bundle:/myapp/vendor/bundle
environment:
TZ: Asia/Tokyo
RAILS_ENV: development
ports:
- "3000:3000"
depends_on:
- db
front:
build:
context: ./frontend/
dockerfile: Dockerfile
volumes:
- ./frontend:/usr/src/app
command: sh -c "cd react-app && yarn start"
ports:
- "8000:3000"
volumes:
mysql-data:
$ mkdir backend
$ touch backend/Dockerfile
$ touch backend/entrypoint.sh
$ touch backend/Gemfile
$ touch backend/Gemfile.lock
./backend/Dockefile
FROM ruby:2.6.6
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs
ENV APP_PATH /myapp
RUN mkdir $APP_PATH
WORKDIR $APP_PATH
COPY Gemfile $APP_PATH/Gemfile
COPY Gemfile.lock $APP_PATH/Gemfile.lock
RUN bundle install
COPY . $APP_PATH
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]
sh:./backend/entrypoint.sh
#!/bin/bash
set -e
# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid
# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"
./backend/Gemfile
source 'https://rubygems.org'
gem 'rails', '~> 6.0.2'
rb:./backend/Gemfile.lock
Gemfile.lock is empty and OK.
$ mkdir frontend
$ touch frontend/Dockerfile
./frontend/Dockerfile
FROM node:14.4.0-alpine3.10
WORKDIR /usr/src/app
Now that we're ready, let's create it from the Rails app first.
rails new
$ docker-compose run api rails new . --force --no-deps -d mysql --api
Created in API mode as described above.
Since it is not possible to connect to the database in the default state, rewrite a part of "./backend/config/database.yml".
yml:./backend/config/database.yml
default: &default
adapter: mysql2
encoding: utf8
pool: <%= ENV.fetch('RAILS_MAX_THREADS') { 5 } %>
username: root
password: password #It should be blank by default, so add it
host: db #The default is "localhost", so rewrite
Next, we will create a React app.
create-react-app
$ docker-compose build
$ docker-compose run front sh -c 'npx create-react-app react-app --template typescript'
Start the container to see if it works.
$ docker-compose up -d
$ docker-compose run api bundle exec rake db:create
localhost:3000
localhost:8000
If you access "localhost: 3000" and "localhost: 8000" respectively and the familiar screen is displayed, it is successful.
It's finally time to start from here. We will create the API for the backend part.
$ docker-compose run api rails g model Todo title:string
$ docker-compose run api rails db:migrate
rb:./backend/app/models/todo.rb
class Todo < ApplicationRecord
validates :title, presence: true
end
Don't forget to set the validation.
$ docker-compose run api rails g controller Api::V1::Todos
ruby:./backend/app/controllers/api/v1/todos_controller.rb
class Api::V1::TodosController < ApplicationController
def index
render json: Todo.all
end
def create
todo = Todo.create(todo_params)
render json: todo
end
def destroy
Todo.destroy(params[:id])
end
private
def todo_params
params.require(:todo).permit(:title)
end
end
ruby:./backend/config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :todos, only: [:index, :create, :destroy]
end
end
end
ruby:./backend/db/seeds.rb
puts 'Creating todos...'
5.times do |i|
Todo.create(title: "Todo#{i + 1}")
end
puts '...Finished!'
Create initial data for operation check.
$ docker-compose run api rails db:seed
$ curl -X GET http://localhost:3000/api/v1/todos
[
{
"id": 1,
"title": "Todo1",
"created_at": "xxxx-xx-xxTxx:xx:xx.xxxZ",
"updated_at": "xxxx-xx-xxTxx:xx:xx.xxxZ"
},
{
"id": 2,
"title": "Todo2",
"created_at": "xxxx-xx-xxTxx:xx:xx.xxxZ",
"updated_at": "xxxx-xx-xxTxx:xx:xx.xxxZ"
},
{
"id": 3,
"title": "Todo3",
"created_at": "xxxx-xx-xxTxx:xx:xx.xxxZ",
"updated_at": "xxxx-xx-xxTxx:xx:xx.xxxZ"
},
{
"id": 4,
"title": "Todo4",
"created_at": "xxxx-xx-xxTxx:xx:xx.xxxZ",
"updated_at": "xxxx-xx-xxTxx:xx:xx.xxxZ"
},
{
"id": 5,
"title": "Todo5",
"created_at": "xxxx-xx-xxTxx:xx:xx.xxxZ",
"updated_at": "xxxx-xx-xxTxx:xx:xx.xxxZ"
}
]
If you can confirm that the data is inserted firmly, it is successful.
In this configuration, the backend and frontend are completely separated, so Rails and React are launched with different port numbers. (Localhost: 3000 and localhost: 8000)
In this case, please note that the API on the Rails side cannot be used from the React side due to security issues in the default state.
In order to be able to use it, "CORS (Cross Origin Resource Sharing)" must be set.
./backend/Gemfile
gem 'rack-cors'
There is a gem that makes it easy to set up CORS, so let's install it.
If you are creating in API mode, it is already described in the Gemfile, so you can uncomment it.
$ docker-compose build
I updated the Gemfile, so build it again.
ruby:./backend/config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'localhost:8000'
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end
The configuration file should already exist, so edit it so that it can be accessed from "localhost: 8000".
$ docker-compose restart
Restart the container for the configuration changes to take effect.
In order to use the API created earlier, we will create the front end part.
As I work from now on, the various automatically generated files will inevitably get in the way, so I will delete them.
I will not use these this time, so you can delete them.
Since the problem will occur due to the effect of deleting some files earlier, rewrite the existing file.
ts:./frontend/react-app/src/App.tsx
const App: React.FC = () => {
return (
<h1>Hello React!</h1>
);
}
export default App;
ts:./frontend/react-app/src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
It's OK if the default screen changes to "Hello React!".
Prepare a "components" folder under "./frontend/react-app/src/" and place each component there.
$ mkdir ./frontend/react-app/src/components
$ touch ./frontend/react-app/src/components/Types.d.ts
$ touch ./frontend/react-app/src/components/TodoForm.tsx
$ touch ./frontend/react-app/src/components/TodoList.tsx
$ touch ./frontend/react-app/src/components/TodoItem.tsx
--Types: Type definition --Header: Header --TodoForm: Input form --TodoList: Todolist --TodoItem: Todo body
ts:./frontend/react-app/src/components/Types.d.ts
interface Todo {
id: number;
title: string;
}
type GetTodos = () => void;
ts:./frontend/react-app/src/components/TodoForm.tsx
import React, { useState } from 'react';
interface TodoFormProps {
getTodos: GetTodos;
}
export const TodoForm: React.FC<TodoFormProps> = ({ getTodos }) => {
const [title, setTitle] = useState<string>('');
const createTodo = (e: React.FormEvent<HTMLFormElement>) => {
fetch('http://localhost:3000/api/v1/todos', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: title,
})
})
.then((response) => {
return response.json();
})
.then(() => {
getTodos();
})
.catch((error) => {
console.log(error);
})
.finally(() => {
setTitle('');
});
e.preventDefault();
}
return (
<form onSubmit={createTodo}>
<input
type='text'
value={title}
onChange={(e) => {
setTitle(e.target.value)
}}
/>
<input type='submit' value='Add' disabled={!title} />
</form>
);
};
ts:./frontend/react-app/src/components/TodoList.tsx
import React from 'react';
import { TodoItem } from './TodoItem';
interface TodoListProps {
todos: Todo[];
getTodos: GetTodos;
}
export const TodoList: React.FC<TodoListProps> = ({ todos, getTodos }) => {
return (
<table>
<thead>
<tr>
<th>Todos</th>
<th></th>
</tr>
</thead>
<tbody>
{
todos.map((todo, index) => {
return (
<TodoItem
key={index}
todo={todo}
getTodos={getTodos}
/>
);
})
}
</tbody>
</table>
);
};
ts:./frontend/react-app/src/components/TodoItem.tsx
import React from 'react';
interface TodoItemProps {
todo: Todo;
getTodos: GetTodos;
}
export const TodoItem: React.FC<TodoItemProps> = ({ todo, getTodos }) => {
const deleteTodo = (id: number) => {
fetch(`http://localhost:3000/api/v1/todos/${todo.id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
})
.then((response) => {
return response;
})
.then(() => {
getTodos();
})
.catch((error) => {
console.error(error);
});
}
return (
<tr>
<td>{todo.title}</td>
<td>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</td>
</tr>
);
};
Edit "./frontend/react-app/src/App.tsx" so that you can call each component.
ts:./frontend/react-app/src/App.tsx
import React, { useState, useEffect } from 'react';
import { TodoList } from './components/TodoList';
import { TodoForm } from './components/TodoForm';
const initialTodos: Todo[] = [];
const App: React.FC = () => {
const [todos, setTodos] = useState(initialTodos);
useEffect(() => {
getTodos();
}, [setTodos]);
const getTodos = () => {
fetch('http://localhost:3000/api/v1/todos')
.then((response) => {return response.json()})
.then((todos) => {setTodos(todos)});
}
return (
<>
<h1>Todo App</h1>
<TodoForm getTodos={getTodos} />
<TodoList todos={todos} getTodos={getTodos} />
</>
);
}
export default App;
If it looks like this in the end, it is complete.
For the time being, I couldn't give a detailed explanation because it was like a memo, but I would like to add a detailed explanation later when I have time.
Recommended Posts