###### 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)