Learn プログ

-マナビメモ-

Riot.js Redux Rails 5 / 8 「Todoリストを表示する」

はじめに

フロントにRiot.jsとReduxを使いサーバーサイドにRailsを使い、初めに環境構築し、次にReduxの使い方の流れをみて、最終的に簡単なTodoAppを作成する。

環境構築

簡単な例でReduxの流れをみる

Todo Appの作成

最終的に完成したTodo App
https://github.com/atfeo/Riot_Redux_Rails

参考

準備

今まで作ってきたsample-outputタグ、title-formタグは使わないのでindex.html.erbから

# ./app/views/top/index.html.erb
<sample-output></sample-output>
<title-form></title-form>

これらを削除し空にする。またindex.jsから

// ./client/src/index.js
import './tags/sample-output.tag';
import './tags/title-form.tag';

title: 'Default title'

'CHANGE_TITLE':
      return Object.assign({}, state, { title: action.data });

も削除。するとこうなる

// ./client/src/index.js
import riot from 'riot';
import { createStore } from 'redux';
 
function reducer(state = {  }, action) {
  switch (action.type) {
    case 
    default:
      return state;
  }
}

const reduxStore = createStore(reducer);
 
document.addEventListener('DOMContentLoaded', () => {
  riot.mount('*', { store: reduxStore });
});

またactions.jsから

// ./client/src/actions.js
changeTitle,

function changeTitle(newTitle) {
  return { type: 'CHANGE_TITLE', data: newTitle };
}

も削除。するとこうなる

// ./client/src/actions.js
module.exports = {
  
};

これで準備完了。

RailsAPIサーバーを作成

始めにRailsJSONを返すAPIサーバーを作成する。

ターミナルでctrl-cを入力しforemanを一時終了。

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

indexアクションを追加し、次に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"}]}が表示されている。

redux-thunk

(redux-thunkについては「なんか非同期処理に必要なんだな」ぐらいの理解しかない。)

redux-thunkを使うための準備としてindex.jsを以下のようにする。

// ./client/src/index.js
// 追加
import thunk from 'redux-thunk';

// 書き換え
import { createStore, applyMiddleware } from 'redux';

const reduxStore = createStore(
  reducer,
  applyMiddleware(thunk)
);

todo-appタグ

Todo Appのメインとなるtodo-appタグを作成。

<!-- ./client/src/tags/todo-app.tag -->
<todo-app>

  <script>
    const actions = require('../actions.js')
    const store = this.opts.store

    store.subscribe(() => {
      this.state = store.getState()
      this.update()
    })
  </script>
</todo-app>

todoのリストはtask-listタグで表示させる。

// ./client/src/tags/todo-app.tag 
<todo-app>
  // 追加
  <task-list tasks={this.state.tasks}></task-list>

  <script>
    const actions = require('../actions.js')
    const store = this.opts.store

    store.subscribe(() => {
      this.state = store.getState()
      this.update()
    })
  </script>
</todo-app>

task-listタグ

// ./client/src/tags/task-list.tag
<task-list>
  <ul>
    <li each={task in this.opts.tasks}>
      {task.name}
    </li>
  </ul>
</task-list>

サーバーからの読み込み

サーバーからデータを読み込むためのメソッドを作成する。

// ./client/src/action.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 };
}

(npmでjQueryをインストールしていないがRailsに入っているので良きに計らってくれると思い、ajaxをそのまま書いた)

タグをマウントするための処理と、アクションを送られたときの処理をindex.jsに書く。

// ./client/src/index.js
// 追加
import './tags/todo-app.tag';
import './tags/task-list.tag';

// 書き換え
function reducer(state = { tasks: [] }, action) {
  switch (action.type) {
    case 'TASKS_LOADED':
      return Object.assign({}, state, { tasks: action.data });
    default:
      return state;
  }
}

document.addEventListener('DOMContentLoaded', () => {
  riot.mount('todo-app', { store: reduxStore });
});

index.html.erbにタグを書く。

# ./app/views/top/index.html.erb
<todo-app></todo-app>

タグがマウントされた時にloadTasks()が実行されるようにtodo-appタグのscriptに以下を追加。

// ./client/tags/todo-app.tag
this.on('mount', ()=> {
  store.dispatch(actions.loadTasks())
})

ブラウザでlocalhost:5000を更新して確認。先程登録したデータが表示される。

ロード時のアニメーションをつける

参考動画に沿って擬似的にロード時間を設け、ロード時のアニメーションを表示出来るようにする。アニメーションはデータ読み込み後消えるようにする。

まず2秒間の擬似的なロード時間を設けるために、actions.jsを書き換える。

// ./client/src/actions.js
function loadTasks() {
  return (dispatch) => {
    $.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/tags/loading-indicator.tag -->
<loading-indicator>
  <img src="/assets/loading.gif" show={this.opts.loading}>
</loading-indicator>

loading-indicatorタグをtodo-appタグにマウント。

<!-- ./client/src/tags/todo-app.tag -->
<loading-indicator loading={this.state.isLoading}></loading-indicator>
<task-list

loading-indicatorをマウントできるようにindex.jsで読み込む。

// ./client/src/index.js
import './tags/loading-indicator.tag';

状態の変化のためのメソッド(アクション)を作成。

// ./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 });

ここまでで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が消える。

サーバーからデータを読み込んで表示することが出来た。
次回はフォームを作成し、新しくタスクを追加できるようにする。

Riot.js Redux Rails 6 / 8に続く