Learn プログ

-マナビメモ-

React Redux Rails 2 / 5 「Todoリストを表示する」

はじめに

Riot.js Redux Railsシリーズで作ったTodoアプリのRiot.js部分をReactを使って書いた。

Riot.js Redux Railsで作ったTodoアプリと全く同じものを作成するのでコードが大分重なっている(Riot.js部分をReactにしただけ)が、今回の一連の記事だけ読んでも出来るように省略はしない(ほぼコピペだし)。

環境構築

Todo Appの作成

最終的に完成した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>
    );
  }
}

RailsAPIサーバーを作成

RailsJSONを返す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が消える。

React Redux Rails 3 / 5へ続く