React Redux Rails 2 / 5 「Todoリストを表示する」
はじめに
Riot.js Redux Railsシリーズで作ったTodoアプリのRiot.js部分をReactを使って書いた。
Riot.js Redux Railsで作ったTodoアプリと全く同じものを作成するのでコードが大分重なっている(Riot.js部分をReactにしただけ)が、今回の一連の記事だけ読んでも出来るように省略はしない(ほぼコピペだし)。
環境構築
- React Redux Rails 1 / 5 「環境構築」
Todo Appの作成
- React Redux Rails 2 / 5 「Todoリストを表示する」
- React Redux Rails 3 / 5 「新たなTodoを追加出来るようにする」
- React Redux Rails 4 / 5 「完了したTodoをチェック出来るようにする」
- React Redux Rails 5 / 5 「チェックしてあるタスクを削除出来るようにする」
最終的に完成したTodo App
https://github.com/atfeo/React_Redux_Rails
参考
Rails
todo-appコンポーネントをマウントするトップページを作成
$ rails g controller Top index
routes.rbを書き換える
# .config/routes.rb Rails.application.routes.draw do root 'top#index' end
次にindex.html.erbにreactの導入部を書く
# ./app/views/top/index.html.erb <div id="content"></div>
index.jsx
以下のようになった。
// ./client/src/index.jsx import React from 'react'; import { render } from 'react-dom'; import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import { Provider } from 'react-redux'; function reducer(state = { tasks: [] }, action) { return state; } const reduxStore = createStore( reducer, applyMiddleware(thunk) ); document.addEventListener('DOMContentLoaded', () => { render( <Provider store={reduxStore}> <TodoApp /> </Provider>, document.getElementById('content') ); });
reducerを作成し、そのreducerや非同期処理に必要なredux-thunkを持ったStoreを作成。作成されたStoreをreact-reduxのProviderを使ってtodo-appコンポーネントに渡す。
コンポーネントの作成
todo-appコンポーネント
Todo Appのメインとなるtodo-appコンポーネントを作成。
// ./client/src/components/todo-app.jsx import React from 'react'; import { connect } from 'react-redux'; import actions from '../actions.js'; import TaskList from './task-list.jsx'; class TodoApp extends React.Component { constructor(props) { super(props); } render() { return ( <div> <TaskList tasks={this.props.tasks} /> </div> ); } } function mapStateToProps(state) { const { tasks } = state; return { tasks, }; } export default connect(mapStateToProps)(TodoApp);
react-reduxのconnectを使うとReduxのsubscribeが必要ないらしい(react-reduxがよくわかっていない)。 todoのリストはtask-listコンポーネントで表示させる。
task-listコンポーネント
// ./client/src/components/task-list.jsx import React from 'react'; export default class TaskList extends React.Component { constructor(props) { super(props); } render() { return ( <ul> {this.props.tasks.map(task => ( <li key={task.id}> {task.name} </li> ))} </ul> ); } }
RailsでAPIサーバーを作成
Controller
まずターミナルで
$ rails g controller api/tasks --no-assets
作成したtasks_controller.rbを
# ./app/controllers/api/tasks_controller.rb class Api::TasksController < ApplicationController def index @tasks = Task.all end end
このようにし、次にroutes.rbに
# ./config.routes.rb namespace :api, format: 'json' do resources :tasks end
を追加。
Model
次にまたターミナルで
$ rails g model task name:string
日時_create_tasks.rbに
# ./db/migrate/日時_create_tasks.rb class CreateTasks < ActiveRecord::Migration[5.0] def change create_table :tasks do |t| t.string :name, null: false t.timestamps end end end
のように , null: false (nameが空だと登録できないようにしている)を追加しターミナルで
$ rails db:migrate
する。また、task.rbにvalidationを追加(同じくnameが空にならないように。しかし、apiとして使う場合モデルのバリデーションは必要ないかもしれない。わからない。)。
# ./app/models/task.rb class Task < ApplicationRecord validates :name, presence: true end
次にseeds.rbファイルに
# ./db/seeds.rb Task.create(name: 'Lesson outline') Task.create(name: 'Record video')
を追加し、ターミナルで
$ rails db:seed
を実行しデータを登録しておく。
View(JSON)
次にJSONを返すindex.json.jbuilderを作成。
# ./app/views/api/tasks/index.json.jbuilder json.tasks(@tasks) { |t| json.extract! t, :id, :name }
ターミナルで
$ foreman start
し、localhost:5000/api/tasksにアクセスすると {"tasks":[{"id":1,"name":"Lesson outline"},{"id":2,"name":"Record video"}]}が表示されている。
サーバーからの読み込み
actions.jsにサーバーからデータを読み込むためのメソッドを作成する。
// ./client/src/actions.js module.exports = { loadTasks, }; function loadTasks() { return (dispatch) => { $.ajax({ url: '/api/tasks.json', dataType: 'json', success: (res) => { dispatch(tasksloaded(res.tasks)); }, error: (xhr, status, err) => { console.log('/api/tasks.json', status, err.toString()); }, }); }; } function tasksloaded(tasks) { return { type: 'TASKS_LOADED', data: tasks }; }
index.jsxのreducerにアクションが渡されたときの処理を追加。
// ./client/src/index.jsx export default function reducer(state = { tasks: [] }, action) { switch (action.type) { case 'TASKS_LOADED': return Object.assign({}, state, { tasks: action.data }); default: return state; } }
todo-app.jsxにtodo-appコンポーネントがマウントされた時にする処理を追加する。
// ./client/src/components/todo-app.jsx componentDidMount() { this.props.dispatch(actions.loadTasks()) }
ここで一度、ブラウザでlocalhost:5000を更新して確認。先程登録したデータが表示される。
ロード時のアニメーションをつける
参考動画に沿って擬似的にロード時間を設け、ロード時のアニメーションを表示出来るようにする。アニメーションはデータ読み込み後消えるようにする。
まず2秒間の擬似的なロード時間を設けるために、actions.jsを書き換える。
// ./client/src/actions.js function loadTasks() { return (dispatch) => { dispatch(toggleLoading(true)); $.ajax({ url: '/api/tasks.json', dataType: 'json', success: (res) => { // 書き換え setTimeout(() => { dispatch(tasksloaded(res.tasks)); }, 2000) }, error: (xhr, status, err) => { console.log('/api/tasks.json', status, err.toString()); }, }); }; }
参考動画のソースのloading.gifをapp/assets/imagesにおく。
次にloading-indicatorコンポーネントを作成。
// ./client/src/components/loading-indicator.jsx import React from 'react'; export default class LoadingIndicator extends React.Component { render() { return ( <div> <img src="/assets/loading.gif" alt="loading-indicator" /> </div> ); } }
loading-indicatorコンポーネントをtodo-appコンポーネントで読み込んでマウント。
// ./client/src/components/todo-app.jsx import LoadingIndicator from './loading-indicator.jsx'; {this.props.isLoading ? <LoadingIndicator /> : null} <TaskList tasks={this.props.tasks} /> function mapStateToProps(state) { const { tasks, isLoading } = state; return { tasks, isLoading, }; }
状態の変化のためのメソッド(アクション)を作成。
// ./client/src/actions.js function toggleLoading(isLoading) { return { type: 'TOGGLE_LOADING', data: isLoading }; }
アクションが渡された時の処理をreducerに追加。
// ./client/src/index.js case 'TOGGLE_LOADING': return Object.assign({}, state, { isLoading: action.data }); default:
ここまででloading-indicatorの用意は出来たので、loadTasks()にdispatchメソッドでアクションを送る処理を追加。
// ./client/src/actions.js function loadTasks() { return (dispatch) => { // 追加 dispatch(toggleLoading(true)); $.ajax({ url: '/api/tasks.json', dataType: 'json', success: (res) => { setTimeout(() => { dispatch(tasksloaded(res.tasks)); // 追加 dispatch(toggleLoading(false)); }, 2000) }, error: (xhr, status, err) => { // 追加 dispatch(toggleLoading(false)); console.log('/api/tasks.json', status, err.toString()); }, }); }; }
ブラウザでlocalhost:5000を更新して確認。
まずloading-indicatorが表示され、2秒後にデータの表示とともにloading-indicatorが消える。