怖くない!React&Railsで作るTODOアプリ 実装編

みなさんはじめまして。
株式会社BitStarでエンジニアインターン
をしている塩原です。

下準備編がまだの人はこちら
[怖くない!ReactとRailsで作るTODOアプリ 下準備編 - Qiita](https://qiita.com/MotohiroSiobara/items/48967bbc353b1d428761)

さて今回でいよいよ実装へ入っていきたいと思います
まずその前にwebpackに関連するファイルの変更をする際にはwebpackのサーバーを立てておかなくてはならないので、ターミナルの別タブで以下のコマンドを走らせておいてください

```
$ npm run webpack-watch
```

仕様を分割して以下のステップでやっていきます

- とりあえずリストを表示する
- リストを追加する
- チェックした情報を保持する
- サーバーに送信してデータを保存する
- データベースにあるリストを初期表示する

# とりあえず表示

- frontent/index.js

```js
import React, { Component } from 'react'
import { render } from 'react-dom'

class App extends Component {
  render() {
    const { todoLists } = this.props
    return (
      <div>
        <ul>
          {this.listCreator(todoLists)}
        </ul>
      </div>
    )
  }

  listCreator(todoLists) {
    let lists = []

    todoLists.forEach((todo, i, todoLists) => {
      let idName = `todo_input_${i}`

      lists.push(
        <li key={i}>
          <input key={i} type='checkbox' value={todo['title']} id={idName} />
          <label key={i + 1} htmlFor={idName}>
            {todo['title']}
          </label>
        </li>
      )
    })

    return lists
  }
}

render(
  <App todoLists={[{title: "テスト"}, {title: "テスト2"}]} />,
  document.getElementById('container'))
```

`render(Reactコーポネント, 表示場所)`という風にかくとhtml側の`<div id="container"></div>`の中にReactコンポーネントで作成した仮想DOMを生成することが可能になります

`todoLists={[{title: "テスト"}, {title: "テスト2"}]}`という部分でpropsを渡すことでclass Appの中で`this.props.todoLists`という風に利用することができます

- app/view/todo_lists/index.html.erb

```html+erb
<h1>TODOアプリケーション</h1>
<div id="container"></div>
<%= javascript_include_tag 'webpack/react_components' %>
```

![スクリーンショット 2017-12-02 12.41.06.png](https://qiita-image-store.s3.amazonaws.com/0/114212/8b34576f-9cba-a6ca-4e88-615c0b1985ae.png)

# リストを追加する
さて次はリストを追加する機能を実装して行きましょう

- frontend/index.js

完成するコードはこんな感じ

```js
import React, { Component } from 'react'
import { render } from 'react-dom'

class App extends Component {
  constructor(props) {
    super(props);
    this.state = { todoLists: props.todoLists, value: '' }
  }

  render() {
    return (
      <div>
        <ul>
          {this.listCreator(this.state.todoLists)}
        </ul>
        <div>
          <h3>TODOリストに追加する</h3>
          <form onSubmit={(e) => this.handleSubmit(e)}>
          <input type='text' value={this.state.value} onChange={(e) => this.handleChange(e)} />
          <input type="submit" value="追加" />
          </form>
        </div>
      </div>
    )
  }

  listCreator(todoLists) {
    let lists = []

    todoLists.forEach((todo, i, todoLists) => {
      let idName = `todo_input_${i}`

      lists.push(
        <li key={i}>
          <input key={i} type='checkbox' value={todo['title']} id={idName} />
          <label key={i + 1} htmlFor={idName}>
            {todo['title']}
          </label>
        </li>
      )
    })

    return lists
  }

  handleSubmit(e) {
    e.preventDefault()

    let title = this.state.value

    let todoLists = [
      ...this.state.todoLists,
      {
        title: title
      }
    ]

    this.setState({ todoLists: todoLists, value: '' })
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }
}

render(
    <App todoLists={[{title: "テスト"}, {title: "テスト2"}]} />,
    document.getElementById('container'))
```

![react機能GIF.gif](https://qiita-image-store.s3.amazonaws.com/0/114212/031ffabf-9ad9-cd8d-b941-444ba21c508b.gif)

こんな風に追加ができるようになる

この部分で外部から渡されたpropsをstateに変換しています
なぜこんなことをするのかというとReactには以下の概念があります
propsはimmutableであり
stateはmutableであるというような概念があります
以下の記事などで詳しく説明してあります
[React における State と Props の違い - Qiita](https://qiita.com/kyrieleison/items/78b3295ff3f37969ab50)

```js
constructor(props) {
  super(props);
  this.state = { todoLists: props.todoLists, value: '' }
}
```

追加機能のjsxの部分で`onSubmit`と`onChange`が設定されています

```html
<div>
  <h3>TODOリストに追加する</h3>
  <form onSubmit={(e) => this.handleSubmit(e)}>
    <input type='text' value={this.state.value} onChange={(e) => this.handleChange(e)} />
    <input type="submit" value="追加" />
  </form>
</div>
```

二つの関数はそれぞれstateを更新するためのものです

```js
  // 送信したtodoリストを追加する
  handleSubmit(e) {
    e.preventDefault()

    let title = this.state.value

    let todoLists = [
      ...this.state.todoLists,
      {
        title: title
      }
    ]

    this.setState({ todoLists: todoLists, value: '' })
  }
  
  // inputフォームに入力した値を常にstateとして持つ
  handleChange(event) {
    this.setState({value: event.target.value});
  }
```

# チェック情報を保持する

```js
import React, { Component } from 'react'
import { render } from 'react-dom'

class App extends Component {
  constructor(props) {
    super(props);
    this.state = { todoLists: props.todoLists, value: '' }
  }

  render() {
    return (
      <div>
        <ul>
          {this.listCreator(this.state.todoLists)}
        </ul>
        <div>
          <h3>TODOリストに追加する</h3>
          <form onSubmit={(e) => this.handleSubmit(e)}>
          <input type='text' value={this.state.value} onChange={(e) => this.handleChange(e)} />
          <input type="submit" value="追加" />
          </form>
        </div>
      </div>
    )
  }

  listCreator(todoLists) {
    let lists = []

    todoLists.forEach((todo, i, todoLists) => {
      let idName = `todo_input_${i}`

      lists.push(
        <li key={i}>
          <input key={i} type='checkbox' value={todo['title']} id={idName} onClick={(e) => this.handleCheck(e, i)} />
          <label key={i + 1} htmlFor={idName}>
            {todo['title']}
          </label>
        </li>
      )
    })

    return lists
  }

  handleSubmit(e) {
    e.preventDefault()

    let title = this.state.value

    let todoLists = [
      ...this.state.todoLists,
      {
        title: title,
        checked: false
      }
    ]

    this.setState({ todoLists: todoLists, value: '' })
  }

  handleChange(e) {
    this.setState({value: e.target.value});
  }

  handleCheck(e, index) {
    let stateTodoLists = this.state.todoLists
    let targetTodo = stateTodoLists[index]
    let todoLists = [
      ...stateTodoLists.slice(0, index),
      Object.assign({}, targetTodo, {
        checked: !targetTodo.checked
      }),
      ...stateTodoLists.slice(index + 1)
    ]

    this.setState({ todoLists: todoLists })
  }
}

render(
    <App todoLists={[]} />,
    document.getElementById('container'))
```

先ほどと同じような流れですね
違いとしては追加とは違い、情報の更新のため位置の指定をする必要があります

```html
<input key={i} type='checkbox' value={todo['title']} id={idName} onClick={(e) => this.handleCheck(e, i)} />
```

`handleCheck`にindex情報を渡しています
このindexによってtodListsのどの位置をクリックしたかという情報を引数で渡しているわけです

```js
  handleCheck(e, index) {
    let stateTodoLists = this.state.todoLists
    let targetTodo = stateTodoLists[index]
    let todoLists = [
      ...stateTodoLists.slice(0, index),
      Object.assign({}, targetTodo, {
        checked: !targetTodo.checked
      }),
      ...stateTodoLists.slice(index + 1)
    ]

    this.setState({ todoLists: todoLists })
  }
```

stateを直接更新することはできないため新しくtodoListsの配列を作ってあげる必要があります

```js
let todoLists = [
  ...stateTodoLists.slice(0, index),
  Object.assign({}, targetTodo, {
    checked: !targetTodo.checked
  }),
  ...stateTodoLists.slice(index + 1)
]
```

`Object.assign({}, 更新したいObject, 更新内容)`とすることで更新したいObjectを更新した状態のObjectを返します

```js
let obj_a = {a: 3, b: 4}
let obj_b = Object.assign({}, obj_a, { b: 5})
console.log(obj_b) // {a: 3, b: 5}

```

# サーバーに送信してデータを保存する
さていよいよサーバーに対してデータを送信したいと思います
送信するデータはリストそれぞれのタイトルにcheckしているかどうかの二点を送信します

```js
import React, { Component } from 'react'
import { render } from 'react-dom'

class App extends Component {
  constructor(props) {
    super(props);
    this.state = { todoLists: props.todoLists, value: '' }
  }

  render() {
    return (
      <div>
        <ul>
          {this.listCreator(this.state.todoLists)}
        </ul>
        <div>
          <h3>TODOリストに追加する</h3>
          <form onSubmit={(e) => this.handleSubmit(e)}>
          <input type='text' value={this.state.value} onChange={(e) => this.handleChange(e)} />
          <input type="submit" value="追加" />
          </form>
        </div>
        <div>
          <button onClick={(e) => this.ajaxSubmit(e, this.state.todoLists)}>保存</button>
        </div>
      </div>
    )
  }

  listCreator(todoLists) {
    let lists = []

    todoLists.forEach((todo, i, todoLists) => {
      let idName = `todo_input_${i}`

      lists.push(
        <li key={i}>
          <input key={i} type='checkbox' value={todo['title']} id={idName} onClick={(e) => this.handleCheck(e, i)} />
          <label key={i + 1} htmlFor={idName}>
            {todo['title']}
          </label>
        </li>
      )
    })

    return lists
  }

  handleSubmit(e) {
    e.preventDefault()

    let title = this.state.value

    let todoLists = [
      ...this.state.todoLists,
      {
        title: title,
        checked: false
      }
    ]

    this.setState({ todoLists: todoLists, value: '' })
  }

  handleChange(e) {
    this.setState({value: e.target.value});
  }

  handleCheck(e, index) {
    let stateTodoLists = this.state.todoLists
    let targetTodo = stateTodoLists[index]
    let todoLists = [
      ...stateTodoLists.slice(0, index),
      Object.assign({}, targetTodo, {
        checked: !targetTodo.checked
      }),
      ...stateTodoLists.slice(index + 1)
    ]

    this.setState({ todoLists: todoLists })
  }

  ajaxSubmit(e, todoLists) {
    $.ajax({
    url: '/todo_lists',
    type: 'post',
    contentType: 'application/json',
    data: JSON.stringify({
      todo_lists: todoLists
    })
    }).done(json => {
    })
  }
}

render(
    <App todoLists={[]} />,
    document.getElementById('container'))
```

- app/controller/todo_lists_controller.rb

```rb
class TodoListsController < ApplicationController
  def index
    todolists = TodoList.all.to_a.map do |list|
      { id: list.id, title: list.title, checked: list.checked }
    end
    @state = { todoLists: todolists }
  end

  def create
    params["todo_lists"].each do |param|
      title_param = param[:title]
      checked_param = param[:checked]
      TodoList.create(title: title_param, checked: checked_param)
    end
    redirect_to root_path
  end
end

```

ではカラムも追加しておきましょう

```
$ bundle exec rails g migration add_column_todolists
```

新しくできたmigrationファイルに以下のカラムを追加する

```rb
class AddColumnTodolists < ActiveRecord::Migration[5.0]
  def change
    add_column :todo_lists, :title, :string
    add_column :todo_lists, :checked, :boolean, default: :false
  end
end
```

# データベースのTodoListを初期表示する
さていよいよ最後です
やることは以下

- データベースのtodoListsのデータを初期表示のときに出す
- データベースに保存済みのデータにはidを持たせる

　

- config/routes.rb

```rb
Rails.application.routes.draw do
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
  resources :todo_lists, only: [:index, :create] do
    collection do
      get "fetch"
      get "list"
    end
  end
  root to: "todo_lists#index"
end
```

- app/controllers/todo_lists_controller.rb

```rb
class TodoListsController < ApplicationController
  def index
    todolists = TodoList.all.to_a.map do |list|
      { id: list.id, title: list.title, checked: list.checked }
    end
    @state = { todoLists: todolists }
  end

  def create
    params["todo_lists"].each do |param|
      puts param
      id_param = param["id"]
      title_param = param[:title]
      checked_param = param[:checked]
      if id_param.present?
        TodoList.find(id_param).update(title: title_param, checked: checked_param)
      else
        TodoList.create(title: title_param, checked: checked_param)
      end
    end
    redirect_to root_path
  end

  def list
    @todo_lists = TodoList.all
  end

  def fetch
    state = []
    TodoList.all.each do |todo|
      state << { title: todo.title, checked: todo.checked, id: todo.id }
    end

    render json: state
  end
end
```

fetchアクションで初期表示に使うtodoリストのjsonを返します

- app/views/todo_lists/list.html.erb

```html+erb

<ul>
  <%- @todo_lists.each do |list| %>
    <li><%= list.checked ? "済" : "未" %> : <%= list.title %></li>
  <%- end %>
</ul>

<%= link_to "ToDoListへ", root_path %>
```

- frontend/index.js

```js
import React, { Component } from 'react'
import { render } from 'react-dom'

class App extends Component {

  constructor() {
    super();
    this.state = { todoLists: [], value: '' }
    this.fetchState()
  }

  render() {
    return (
      <div>
        <ul>
          {this.listCreator(this.state.todoLists)}
        </ul>
        <div>
          <h3>TODOリストに追加する</h3>
          <form onSubmit={(e) => this.handleSubmit(e)}>
          <input type='text' value={this.state.value} onChange={(e) => this.handleChange(e)} />
          <input type="submit" value="追加" />
          </form>
        </div>
        <div>
          <button onClick={(e) => this.ajaxSubmit(e, this.state.todoLists)}>保存</button>
        </div>
      </div>
    )
  }

  listCreator(todoLists) {
    let lists = []

    todoLists.forEach((todo, i, todoLists) => {
      let idName = `todo_input_${i}`

      lists.push(
        <li key={i}>
          <input key={i} type='checkbox' id={idName} onChange={(e) => this.handleCheck(e, i)} checked={todo['checked']} />
          <label key={i + 1} htmlFor={idName}>
            {todo['title']}
          </label>
        </li>
      )
    })

    return lists
  }

  handleSubmit(e) {
    e.preventDefault()

    let title = this.state.value

    let todoLists = [
      ...this.state.todoLists,
      {
        title: title,
        checked: false
      }
    ]

    this.setState({ todoLists: todoLists, value: '' })
  }

  handleChange(e) {
    this.setState({value: e.target.value});
  }

  handleCheck(e, index) {
    let stateTodoLists = this.state.todoLists
    let targetTodo = stateTodoLists[index]
    let todoLists = [
      ...stateTodoLists.slice(0, index),
      Object.assign({}, targetTodo, {
        checked: !targetTodo.checked
      }),
      ...stateTodoLists.slice(index + 1)
    ]

    this.setState({ todoLists: todoLists })
  }

  ajaxSubmit(e, todoLists) {
    $.ajax({
    url: '/todo_lists',
    type: 'post',
    contentType: 'application/json',
    data: JSON.stringify({
      todo_lists: todoLists
    })
    }).done(json => {
      location.href = "/todo_lists/list"
    })
  }

  fetchState() {
    $.ajax({
      url: '/todo_lists/fetch',
      type: 'get',
      contentType: 'application/json'
    }).done(json => {
      this.setState({ todoLists: json })
    })
  }
}

render(
    <App />,
    document.getElementById('container'))
```

```js
fetchState() {
    $.ajax({
      url: '/todo_lists/fetch',
      type: 'get',
      contentType: 'application/json'
    }).done(json => {
      this.setState({ todoLists: json })
    })
  }
```

この関数をconstructorで呼び出し、TodoListの初期jsonを取得し、todoListsステートに入れている

![todoアプリ機能紹介.gif](https://qiita-image-store.s3.amazonaws.com/0/114212/9b0e139b-c1bf-8565-9f29-646f0aba417c.gif)

完成！