Rails + Redux-saga+Redux-FormでTodoリストを実装してみた

Reduxクソむずいです。Reactと全然違うじゃないですか！！！

Reduxを勉強中、「どこからAPI呼び出せばいいんやねん！」って思って調べてたら３日くらいたってました。

ReduxでAPIを呼び出すにはいろいろ方法があるらしく`redux-thunk`や`redux-saga`が代表的らしいです。

redux-thunkはactionsにロジックを書いてしまうため肥大化しやすく、それを解決するためにredux-sagaがよく使われます。redux-saga + Redux-formを組み合わせたコードがあまりなかったので簡単なTodoリストを作ってみました(バックエンドはRailsです)

React＋RailsでCRUDを実装したやつ
https://qiita.com/yoshimo123/items/9aa8dae1d40d523d7e5d

GitHubはこちら
https://github.com/yoshimoto8/TodoApp-Redux-Form-Saga

## 読者対象

ReactとReduxチュートリアル終わったレベル
Rails関して知識がある方

## 概要

Railsは完全にAPIで, フロントをReact+ReduxでTodoリストを実装していきます。

見た目はこんな感じ

![todoapp.gif](https://qiita-image-store.s3.amazonaws.com/0/179816/b977eb20-3ad3-5290-d6b1-aca33c5e35c0.gif)

なお編集は実装してません。


##  まずはRails側の実装

Railsはサクッと実装していきます。
アプリケーションの作成

```
rails new TodoApp -d mysql --api
```

必要なファイルの作成

```
rails g controller todo_datas
rails g model todo_data
```

マイグレーションファイルに記入

```ruby
class CreateTodoData < ActiveRecord::Migration[5.1]
  def change
    create_table :todo_data do |t|
      t.text :text

      t.timestamps
    end
  end
end
```

```
rake db:create
rake db:migrate

```

todo_datas_controller.rb

```ruby
class TodoDatasController < ApplicationController
  def index
    @data = TodoDatum.all()
    render json: @data
  end

  def create
    @create_data = TodoDatum.create(text: params[:todoText])
    render json: @create_data
  end 

  def destroy
    @deleted_data= TodoDatum.find(params[:id]).delete
    render json: @deleted_data
  end
end
```

ルーティングに記入 routes.rb

```ruby
Rails.application.routes.draw do
  resources :todo_datas
end
```

シードデータ seed.rb

```ruby
TodoDatum.create(text: 'aaa')
TodoDatum.create(text: 'bbb')
TodoDatum.create(text: 'bbb')
```

```
rake db:seed
```

rack-corsの設定 Gemfile

```ruby
gem 'rack-cors', :require => 'rack/cors'
```

application.rb

```ruby
config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins '*'
        resource '*',
         :headers => :any,
         :expose => ['access-token', 'expiry', 'token-type', 'uid', 'client'],
         :methods => [:get, :post, :patch, :delete, :options, :put, :head]
       end
     end
```


`bundle install`したら、これでpostmanなどで確認するとjsonでデータが返ってくるはずです。

```
rails s -p 3001
```

[http://localhost:3001/todo_datas] こちらのURLにアクセスすると以下のように返ってくると思います。

<img width="591" alt="redux-saga.png" src="https://qiita-image-store.s3.amazonaws.com/0/179816/31a6eda7-98af-6640-eaa6-7dedb0ad3bdc.png">

## React + Reduxでフロント側の実装

```
create-react-app flont
```

srcの構成は以下のようにしています。

```
src
├── App.css
├── App.test.js
├── actions
│   └── index.js
├── components
│   ├── App.js
│   ├── ShowTodo.jsx
│   └── TodoForm.jsx
├── index.css
├── index.js
├── logo.svg
├── reducers
│   ├── Todo.js
│   └── index.js
├── registerServiceWorker.js
└── sagas
    ├── Api
    │   ├── CreateTodoData.js
    │   ├── DeleteTodoData.js
    │   └── FetchTodoData.js
    ├── Todo.js
    └── index.js
```

必要なライブラリのインストール

```
yarn add axios react-redux redux redux-saga
```

### redux-saga

redux-sagaはMiddlewareの一つで

action → Middleware → Reducers →
Middleware→store

という流れでデータが移動していきます。

まずはsagaの実装 sagas/index.js

```javascript
import { takeLatest } from 'redux-saga/effects'
import { REQUEST_FETCH, REQUEST_CREATE, REQUEST_DELETE} from '../actions'
import {fetchData, createData, deleteData} from './Todo'

function* rootSaga() {
  yield [
    takeLatest(REQUEST_FETCH,fetchData),
    takeLatest(REQUEST_CREATE,createData),
    takeLatest(REQUEST_DELETE, deleteData)
  ]
}

export default rootSaga
```

sagas/Todo.js

```javascript
import { put, call } from 'redux-saga/effects'
import {succeededFetch,
         failedFetch,
         succeededCreate,
         failedCreate,
         succeededDelete,
         failedDelete} from '../actions'
import fetchTodoData from './Api/FetchTodoData'
import createTodoData  from './Api/CreateTodoData'
import deleteTodoData from './Api/DeleteTodoData'

export function* fetchData() {
  try {
    const payload = yield call(fetchTodoData)
    yield put(succeededFetch(payload))
  } catch (e) {
    yield put(failedFetch(e.message));
  }
}

export function* createData(action) {
  const textData = action.todoText.location
  const responseData = yield call(createTodoData, textData)
  if (responseData) {
    yield put(succeededCreate(responseData.data))
  } else {
    yield put(failedCreate('エラー'))
  }
}

export function* deleteData(action) {
  const todoId = action.data
  const responseData = yield call(deleteTodoData, todoId)
  if (responseData) {
    yield put(succeededDelete(responseData.data))
  } else {
    yield put(failedDelete('エラー'))
  }
}
```

次にAPIで、axiosで実装していきます。
sagas/Api/FetchTodoData.js

```javascript
import axios from 'axios'

export default function fetchTodoData() {
  return axios({
    method: "get",
    url: "http://localhost:3001/todo_datas"
  })
}
```

sagas/Api/DeleteTodoData.js

```javascript
import axios from 'axios'

export default function fetchTodoData(id) {
  return axios({
    method: "delete",
    url: `http://localhost:3001/todo_datas/${id}`
  })
}
```

sagas/Api/DeleteTodoData.js

```javascript
import axios from 'axios'

const url = "http://localhost:3001/todo_datas"
export default function createTodoData(todoText)  {
  return axios.post(url, {todoText: todoText })
}
```

次にReducersを実装していきます。

Reducers/index.js

```javascript
import { combineReducers } from 'redux'
import { reducer as formReducer } from 'redux-form'
import fetchTodoData from './Todo'

const rootReducer = combineReducers({
  form: formReducer,
  fetchTodoData,
})

export default rootReducer
```

Reducers/Todo.js

```javascript
import { REQUEST_FETCH, SUCCEEDED_FETCH, FAILED_FETCH } from '../actions'

const initialState = {
  fetching: false,
  todoText: [],
  error: null
}

const fetchTodoData = (state=initialState, action) => {
  switch (action.type) {
    case REQUEST_FETCH:
      return { ...state, fetching: true, error: null }
    case SUCCEEDED_FETCH:
      return { ...state, fetching: false, todoText: action.payload.data}
    case FAILED_FETCH:
      return { ...state, fetching: false, todoText: null, error: action.error }
    default:
      return state
  }
}

export default fetchTodoData
```

次にActionsの実装です。

actions/index.js

```javascript
export const REQUEST_FETCH = 'REQUEST_FETCH'
export const SUCCEEDED_FETCH = 'SUCCEEDED_FETCH'
export const FAILED_FETCH = 'FAILED_FETCH'

export const REQUEST_CREATE = 'REQUEST_CREATE'
export const SUCCEEDED_CREATE = 'SUCCEEDED_CREATE'
export const FAILED_CREATE = 'FAILED_CREATE'

export const REQUEST_DELETE = 'REQUEST_DELETE'
export const SUCCEEDED_DELETE = 'SUCCEEDED_DELETE'
export const FAILED_DELETE = 'FAILED_DELETE'

export const requestFetch = () => ({type: REQUEST_FETCH})
export const succeededFetch = payload => ({type: SUCCEEDED_FETCH, payload})
export const failedFetch = message => ({type: FAILED_FETCH, message})

export const requestCreate = todoText => ({type: REQUEST_CREATE, todoText})

export const succeededCreate = payload => ({type: SUCCEEDED_CREATE, payload})
export const failedCreate = message => ({type: FAILED_CREATE, message})

export const requestDelete = data => ({ type: REQUEST_DELETE, data})

export const succeededDelete = payload => ({type: SUCCEEDED_DELETE, payload})
export const failedDelete = message => ({type: FAILED_DELETE, message})
```

次にコンポーネントの実装です。

components/App.jsx

```jsx
import React, { Component } from 'react';
import '../App.css';
import TodoForm from './TodoForm'
import ShowTodo from './ShowTodo'

class App extends Component {
  render() {
    return (
      <div className="App">
        <TodoForm />
        <ShowTodo />
      </div>
    );
  }
}

export default App;
```

components/ShowTodo.jsx

```jsx
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { requestFetch, requestDelete} from '../actions'

class ShowTodo extends Component{
  componentDidMount() {
    this.props.requestFetch()
  }

  render() {
    const datas = this.props.todoText.fetchTodoData.todoText
    return(
      <div>
        {datas.map((data) => {
          return (
            <div key={data.id}>
              {data.text}
              <span Style="margin-left: 20px; color: red;" onClick={() => this.props.requestDelete(data.id)}>x</span>
            </div>
          )
        })}
      </div>
    )
  }
}

const mapDispatchToProps = dispatch => ({
  requestFetch: () => dispatch(requestFetch()),
  requestDelete: (data) => dispatch(requestDelete(data)),
})
const mapStateToProps = state => ({
  todoText: state
})
export default connect(mapStateToProps, mapDispatchToProps)(ShowTodo)
```

components/TodoForm.jsx

```jsx
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Field, reduxForm, reset } from 'redux-form'
import { Input, Button, Message } from 'semantic-ui-react'
import { requestCreate } from '../actions'


class TodoForm extends Component{
  locationInput({ input, meta: { touched, error }, ...custom }) {
    const hasError = touched && error !== undefined
    return (
      <div>
        {hasError &&
          <Message
            error
            header='Error'
            content={error} />
        }
        <Input
          error={hasError}
          fluid
          placeholder="Location..."
          {...input}
          {...custom} />
      </div>
    );
  }

  submit(value, dispatch) {
    dispatch(requestCreate(value))
    dispatch(reset('simple'))
  }
  
  render() {
    const { handleSubmit } = this.props
    return(
      <div>
        <form onSubmit={handleSubmit(this.submit.bind(this))}>
          <Field name="location" component={this.locationInput} />
          <br/>
          <Button type="submit">Submit</Button>
        </form>
      </div>
    )
  }
}


const validate = values => {
  const errors = {}
  if (!values.location || values.location.trim() === '') {
    errors.location = 'Location required'
  }
  return errors
}


const exportTodoForm = reduxForm({
  form: 'simple',
  validate
})(TodoForm)


const mapDispatchToProps = {
  requestCreate,
 }

export default connect(null, mapDispatchToProps)(exportTodoForm)
```

src/index.js

```jsx
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App';
import createSagaMiddleware from 'redux-saga'
import { createStore, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import reducer from './reducers'
import rootSaga from './sagas'


const sagaMiddleware = createSagaMiddleware()
const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware))

sagaMiddleware.run(rootSaga)

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>
, document.getElementById('root'));
```

これでRails側のサーバーを起動しつつ `yarn start`でReact側のサーバーを起動させ以下のURLにアクセスするとTodoリストが出ていると思います。

[http://localhost:3000/]

もしかするとGitHubでみた方がわかりやすいかもしれません。
https://github.com/yoshimoto8/TodoApp-Redux-Form-Saga