###### tags: `YouTube`
# React on Rails
## 私の Mac のバージョン
![](https://i.imgur.com/VQP1446.png)
## 各バージョン
| Ruby | Rails | yarn | react |
| ----- | -------- | ------ | ------ |
| 2.7.0 | 6.1.4.1 | 1.22.4 | 17.0.1 |
## Rails 環境構築 (Mac OS)
### 環境構築流れ
1. rbenv のインストール
2. Ruby のインストール
3. Bundler のインストール
4. yarn のインストール
5. rails のインストール
#### 1. rbenv のインストール
rbenv とは、Ruby のバージョン管理ツールです。
rbenv をインストールするためには、Command Line Tools と Homebrew のインストールが必要です。
Command Line Tools のインストール
```
$ xcode-select --install
```
Homebrew のインストール
```
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
```
Homebrew のアップデート
```
$ brew update
```
Homebrew を使った rbenv のインストール
```
$ brew install rbenv
```
rbenv への PATH を通す
```
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.zshrc
```
rbenv を使うために必要な「rbenv init -」コマンドを設定
```
$ echo 'eval "$(rbenv init -)"' >> ~/.zshrc
```
設定の反映
```
$ source ~/.zshrc
```
※ bashを使っている方は、上記3コマンドの「zshrc」を「bash_profile」に置き換えてください。bash を使っているのか、zsh を使っているのかを確認するためには、`echo $SHELL`を実行してみてください。
`bin/bash` と出力されたら bash 、`bin/zsh` と出力されたら `zsh` です。
#### 2. Ruby のインストール
先ほどの rbenv を使って Ruby の 2.7.0 をインストールします。
Ruby 2.7.0 のインストール
```
$ rbenv install 2.7.0
```
システム全体で使う ruby のバージョンを指定
```
$ rbenv global 2.7.0
```
Ruby のバージョンの確認
```
$ ruby -v
```
#### 3. Bundler のインストール
Bundler とは Gem のバージョン、依存関係を管理するツールです。
Bundler のインストール
```
$ gem install bundler
```
#### 4. yarn のインストール
yarn とは、JavaScript のパッケージマネージャです。Rails6 では Webpacker が標準になったことにより、yarn が必要です。
```
$ brew install yarn
```
#### 5. rails のインストール
Ruby の 6.1.4.1 をインストールします。
```
$ gem install rails -v 6.1.4.1
```
## 使用するgem
使用するgemはありません。
## アプリケーション作成手順
### ステップ0(rails new & yarn add)
```
$ rails new todo_app --webpack=react -T
```
```
$ yarn add react-router-dom axios styled-components react-icons react-toastify
```
> - react-router-dom:Reactでのroutingの実現。
> - axios:サーバとのHTTP通信を行う。
> - styled-components:CSS in JS のライブラリ
> - react-icons:Font Awesomeなどのアイコンが簡単に利用できるライブラリ
### ステップ1(model & table & data)
```
$ rails g model todo name is_completed:boolean
```
```ruby
def change
create_table :todos do |t|
t.string :name, null: false
t.boolean :is_completed, default: false, null: false
t.timestamps
end
end
```
> boolean型を指定するときには必ずdefault値を指定してください。
```
$ rails db:migrate
```
```ruby=seed.rb
SAMPLE_TODOS = [
{
name: 'Going around the world',
},
{
name: 'graduating from college'
},
{
name: 'publishing a book',
}
]
SAMPLE_TODOS.each do |todo|
Todo.create(todo)
end
```
```
$ rails db:seed
```
### ステップ2(site#index)
```ruby=app/controllers/site_controller.rb
class SiteController < ApplicationController
def index
end
end
```
```ruby=app/views/site/index.html.erb
<div id="root"></div>
```
### ステップ3(todos_controller)
```ruby
class Api::V1::TodosController < ApplicationController
def index
todos = Todo.order(updated_at: :desc)
render json: todos
end
def show
todo = Todo.find(params[:id])
render json: todo
end
def create
todo = Todo.new(todo_params)
if todo.save
render json: todo
else
render json: todo.errors, status: 422
end
end
def update
todo = Todo.find(params[:id])
if todo.update(todo_params)
render json: todo
else
render json: todo.errors, status: 422
end
end
def destroy
if Todo.destroy(params[:id])
head :no_content
else
render json: { error: "Failed to destroy" }, status: 422
end
end
def destroy_all
if Todo.destroy_all
head :no_content
else
render json: { error: "Failed to destroy" }, status: 422
end
end
private
def todo_params
params.require(:todo).permit(:name, :is_completed)
end
end
```
```ruby
root to: redirect('/todos')
get 'todos', to: 'site#index'
get 'todos/new', to: 'site#index'
get 'todos/:id/edit', to: 'site#index'
namespace :api do
namespace :v1 do
delete '/todos/destroy_all', to: 'todos#destroy_all'
resources :todos, only: %i[index show create update destroy]
end
end
```
### ステップ4(application_controller)
```ruby
class ApplicationController < ActionController::Base
protect_from_forgery with: :null_session
end
```
### ステップ5(turbolinksの無効化)
```ruby=application.html.erb
<%= stylesheet_link_tag 'application', media: 'all' %>
<%= javascript_pack_tag 'application' %>
```
```ruby=application.js
// Turbolinks.start()
```
### ステップ6(app/views/application.html.erb)
```app/javascripts/packs/hello_react.jsx``` を ```app/javascripts/packs/index.jsx``` に変更。
```ruby=app/views/layouts/application.html.erb
<%= javascript_pack_tag 'index' %>
```
[localhost:3000/todos](http://localhost:3000/todos) にアクセスして、「Hello React!」が表示されればOK。
### ステップ7(componentsフォルダの作成)
- AddTodo.js
- App.js
- EditTodo.js
- TodoList.js
- App.css
### ステップ8(App.css)
```css=App.css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
min-height: 100vh;
color: rgb(1, 1, 1);
}
h1 {
text-align: center;
margin-top: 30px;
margin-bottom: 15px;
}
a {
text-decoration: none;
color: rgb(1, 1, 1);
}
input:focus {
outline: 0;
}
```
### ステップ9(app/javascript/packs/index.jsx)
```javascript
import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from 'react-router-dom';
import App from '../components/App'
document.addEventListener('DOMContentLoaded', () => {
ReactDOM.render(
<BrowserRouter>
<App/>
</BrowserRouter>,
document.querySelector('#root'),
);
});
```
### ステップ10(app/javascript/components/App.js)
```javascript
import React from 'react'
import { Switch, Route, Link } from 'react-router-dom'
import styled from 'styled-components'
import AddTodo from './AddTodo'
import TodoList from './TodoList'
import EditTodo from './EditTodo'
import './App.css'
const Nabvar = styled.nav`
background: #dbfffe;
min-height: 8vh;
display: flex;
justify-content: space-around;
align-items: center;
`
const Logo = styled.div`
font-weight: bold;
font-size: 23px;
letter-spacing: 3px;
`
const NavItems = styled.ul`
display: flex;
width: 400px;
max-width: 40%;
justify-content: space-around;
list-style: none;
`
const NavItem = styled.li`
font-size: 19px;
font-weight: bold;
opacity: 0.7;
&:hover {
opacity: 1;
}
`
const Wrapper = styled.div`
width: 700px;
max-width: 85%;
margin: 20px auto;
`
function App() {
return (
<>
<Nabvar>
<Logo>
TODO
</Logo>
<NavItems>
<NavItem>
<Link to="/todos">
Todos
</Link>
</NavItem>
<NavItem>
<Link to="/todos/new">
Add New Todo
</Link>
</NavItem>
</NavItems>
</Nabvar>
<Wrapper>
<Switch>
<Route exact path="/todos" component={TodoList} />
<Route exact path="/todos/new" component={AddTodo} />
<Route path="/todos/:id/edit" component={EditTodo} />
</Switch>
</Wrapper>
</>
)
}
export default App
```
### ステップ11(app/javascript/components/TodoList.js)
```javascript
import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import axios from 'axios'
import styled from 'styled-components'
import { ImCheckboxChecked, ImCheckboxUnchecked } from 'react-icons/im'
import { AiFillEdit } from 'react-icons/ai'
const SearchAndButtton = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`
const SearchForm = styled.input`
font-size: 20px;
width: 100%;
height: 40px;
margin: 10px 0;
padding: 10px;
`
const RemoveAllButton = styled.button`
width: 16%;
height: 40px;
background: #f54242;
border: none;
font-weight: 500;
margin-left: 10px;
padding: 5px 10px;
border-radius: 3px;
color: #fff;
cursor: pointer;
`
const TodoName = styled.span`
font-size: 27px;
${({ is_completed }) => is_completed && `
opacity: 0.4;
`}
`
const Row = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin: 7px auto;
padding: 10px;
font-size: 25px;
`
const CheckedBox = styled.div`
display: flex;
align-items: center;
margin: 0 7px;
color: green;
cursor: pointer;
`
const UncheckedBox = styled.div`
display: flex;
align-items: center;
margin: 0 7px;
cursor: pointer;
`
const EditButton = styled.span`
display: flex;
align-items: center;
margin: 0 7px;
`
function TodoList() {
const [todos, setTodos] = useState([])
const [searchName, setSearchName] = useState('')
useEffect(() => {
axios.get('/api/v1/todos.json')
.then(resp => {
console.log(resp.data)
setTodos(resp.data);
})
.catch(e => {
console.log(e);
})
}, [])
const removeAllTodos = () => {
const sure = window.confirm('Are you sure?');
if (sure) {
axios.delete('/api/v1/todos/destroy_all')
.then(resp => {
setTodos([])
})
.catch(e => {
console.log(e)
})
}
}
const updateIsCompleted = (index, val) => {
var data = {
id: val.id,
name : val.name,
is_completed: !val.is_completed
}
axios.patch(`/api/v1/todos/${val.id}`, data)
.then(resp => {
const newTodos = [...todos]
newTodos[index].is_completed = resp.data.is_completed
setTodos(newTodos)
})
}
return (
<>
<h1>Todo List</h1>
<SearchAndButtton>
<SearchForm
type="text"
placeholder="Search todo..."
onChange={event => {
setSearchName(event.target.value)
}}
/>
<RemoveAllButton onClick={removeAllTodos}>
Remove All
</RemoveAllButton>
</SearchAndButtton>
<div>
{todos.filter((val) => {
if(searchName === "") {
return val
} else if (val.name.toLowerCase().includes(searchName.toLowerCase())) {
return val
}
}).map((val, key) => {
return (
<Row key={key}>
{val.is_completed ? (
<CheckedBox>
<ImCheckboxChecked onClick={() => updateIsCompleted(key, val) } />
</CheckedBox>
) : (
<UncheckedBox>
<ImCheckboxUnchecked onClick={() => updateIsCompleted(key, val) } />
</UncheckedBox>
)}
<TodoName is_completed={val.is_completed}>
{val.name}
</TodoName>
<Link to={"/todos/" + val.id + "/edit"}>
<EditButton>
<AiFillEdit />
</EditButton>
</Link>
</Row>
)
})}
</div>
</>
)
}
export default TodoList
```
### ステップ12(app/javascript/components/AddTodo.js)
```javascript
import React, { useState } from 'react'
import axios from 'axios'
import styled from 'styled-components'
import { toast } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css'
import { FiSend } from 'react-icons/fi'
const InputAndButton = styled.div`
display: flex;
justify-content: space-between;
margin-top: 20px;
`
const InputName = styled.input`
font-size: 20px;
width: 100%;
height: 40px;
padding: 2px 7px;
`
const Button = styled.button`
font-size: 20px;
border: none;
border-radius: 3px;
margin-left: 10px;
padding: 2px 10px;
background: #1E90FF;
color: #fff;
text-align: center;
cursor: pointer;
${({ disabled }) => disabled && `
opacity: 0.5;
cursor: default;
`}
`
const Icon = styled.span`
display: flex;
align-items: center;
margin: 0 7px;
`
toast.configure()
function AddTodo(props) {
const initialTodoState = {
id: null,
name: "",
is_completed: false
};
const [todo, setTodo] = useState(initialTodoState);
const notify = () => {
toast.success("Todo successfully created!", {
position: "bottom-center",
hideProgressBar: true
});
}
const handleInputChange = event => {
const { name, value } = event.target;
setTodo({ ...todo, [name]: value });
};
const saveTodo = () => {
var data = {
name: todo.name,
};
axios.post('/api/v1/todos', data)
.then(resp => {
setTodo({
id: resp.data.id,
name: resp.data.name,
is_completed: resp.data.is_completed
});
notify();
props.history.push("/todos");
})
.catch(e => {
console.log(e)
})
};
return (
<>
<h1>New Todo</h1>
<InputAndButton>
<InputForName
type="text"
required
value={todo.name}
onChange={handleInputChange}
name="name"
/>
<Button
onClick={saveTodo}
disabled={(!todo.name || /^\s*$/.test(todo.name))}
>
<Icon>
<FiSend />
</Icon>
</Button>
</InputAndButton>
</>
)
}
export default AddTodo
```
> ^:論理行頭
> \s:垂直タブ以外のすべての空白文字。
> *:直前のパターンの0回以上繰り返し(最長一致)
> $:論理行末
### ステップ13(app/javascript/components/EditTodo.js)
```javascript
import React, { useState, useEffect } from "react"
import axios from 'axios'
import styled from 'styled-components'
import { toast } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css'
const InputName = styled.input`
font-size: 20px;
width: 100%;
height: 40px;
padding: 2px 7px;
margin: 12px 0;
`
const CurrentStatus = styled.div`
font-size: 19px;
margin: 8px 0 12px 0;
font-weight: bold;
`
const IsCompeletedButton = styled.button`
color: #fff;
font-weight: 500;
font-size: 17px;
padding: 5px 10px;
background: #f2a115;
border: none;
border-radius: 3px;
cursor: pointer;
`
const EditButton = styled.button`
color: white;
font-weight: 500;
font-size: 17px;
padding: 5px 10px;
margin: 0 10px;
background: #0ac620;
border-radius: 3px;
border: none;
`
const DeleteButton = styled.button`
color: #fff;
font-size: 17px;
font-weight: 500;
padding: 5px 10px;
background: #f54242;
border: none;
border-radius: 3px;
cursor: pointer;
`
toast.configure()
function EditTodo(props) {
const initialTodoState = {
id: null,
name: "",
is_completed: false
};
const [currentTodo, setCurrentTodo] = useState(initialTodoState);
const notify = () => {
toast.success("Todo successfully updated!", {
position: "bottom-center",
hideProgressBar: true
});
}
const getTodo = id => {
axios.get(`/api/v1/todos/${id}`)
.then(resp => {
setCurrentTodo(resp.data);
})
.catch(e => {
console.log(e);
});
};
useEffect(() => {
getTodo(props.match.params.id);
console.log(props.match.params.id)
}, [props.match.params.id]);
const handleInputChange = event => {
const { name, value } = event.target;
setCurrentTodo({ ...currentTodo, [name]: value });
};
const updateIsCompleted = (val) => {
var data = {
id: val.id,
name: val.name,
is_completed: !val.is_completed
};
axios.patch(`/api/v1/todos/${val.id}`, data)
.then(resp => {
setCurrentTodo(resp.data);
})
};
const updateTodo = () => {
axios.patch(`/api/v1/todos/${currentTodo.id}`, currentTodo)
.then(response => {
notify();
props.history.push("/todos");
})
.catch(e => {
console.log(e);
});
};
const deleteTodo = () => {
const sure = window.confirm('Are you sure?');
if (sure) {
axios.delete(`/api/v1/todos/${currentTodo.id}`)
.then(resp => {
console.log(resp.data);
props.history.push("/todos");
})
.catch(e => {
console.log(e);
});
}
};
return (
<>
<h1>Editing Todo</h1>
<div>
<div>
<label htmlFor="name">Current Name</label>
<InputForName
type="text"
id="name"
name="name"
value={currentTodo.name}
onChange={handleInputChange}
/>
<div>
<span>CurrentStatus</span><br/>
<CurrentStatus>
{currentTodo.is_completed ? "Completed" : "UnCompleted"}
</CurrentStatus>
</div>
</div>
{currentTodo.is_completed ? (
<IsCompeletedButton
className="badge badge-primary mr-2"
onClick={() => updateIsCompleted(currentTodo)}
>
UnCompleted
</IsCompeletedButton>
) : (
<IsCompeletedButton
className="badge badge-primary mr-2"
onClick={() => updateIsCompleted(currentTodo)}
>
Completed
</IsCompeletedButton>
)}
<EditButton
type="submit"
onClick={updateTodo}
>
Update
</EditButton>
<DeleteButton
className="badge badge-danger mr-2"
onClick={deleteTodo}
>
Delete
</DeleteButton>
</div>
</>
);
};
export default EditTodo
```
## 参考文献
[Search Filter React Tutorial - Search Bar in React](https://www.youtube.com/watch?v=mZvKPtH9Fzo&t=755s
)
[React Hooks CRUD example with Axios and Web API
](https://bezkoder.com/react-hooks-crud-axios-api/)
[React Todo List App Tutorial - Beginner React JS Project Using Hooks
](https://www.youtube.com/watch?v=E1E08i2UJGI&t=2493s)