Reduxクソむずいです。Reactと全然違うじゃないですか!!!
Reduxを勉強中、「どこからAPI呼び出せばいいんやねん!」って思って調べてたら3日くらいたってました。
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リストを実装していきます。
見た目はこんな感じ
なお編集は実装してません。
まずは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にアクセスすると以下のように返ってくると思います。
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