20
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-04-06

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

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

ReduxでAPIを呼び出すにはいろいろ方法があるらしくredux-thunkredux-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

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

まずはRails側の実装

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

rails new TodoApp -d mysql --api

必要なファイルの作成

rails g controller todo_datas
rails g model todo_data

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

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

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

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

シードデータ seed.rb

TodoDatum.create(text: 'aaa')
TodoDatum.create(text: 'bbb')
TodoDatum.create(text: 'bbb')
rake db:seed

rack-corsの設定 Gemfile

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

application.rb

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にアクセスすると以下のように返ってくると思います。

redux-saga.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

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

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

import axios from 'axios'

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

sagas/Api/DeleteTodoData.js

import axios from 'axios'

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

sagas/Api/DeleteTodoData.js

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

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

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

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

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

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

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

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

20
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?