Learn プログ

-マナビメモ-

React Redux Rails 3 / 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を追加する際のサーバー側の処理を作成する。

Controller

tasks_controller.rbに

# ./app/controllers/api/tasks_controller.rb
def create
  @task = Task.new(task_params)
  if @task.save
    render :show, status: :created
  else
    head :unprocessable_entity
  end
end

private

def task_params
  params.permit(:name)
end

を追加。

View(JSON)

保存に成功したときにJSON返すshow.json.jbuilderを作成。

# ./app/views/api/tasks/show.json.jbuilder
json.extract! @task, :id, :name

これでサーバー側は完成。

Todoを追加するためのtask-formコンポーネント

UIをRiot公式のTodoのように作成する。
+ フォームに入力があった時にボタンが有効になるようにする。 + 何個目のTodoを登録しようとしているかわかるようにする。

// ./client/components/task-form.jsx
import React from 'react';

export default class TaskForm extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.handleKeyup = this.handleKeyup.bind(this);
  }

  handleSubmit(e) {
    e.preventDefault();
    this.props.addtask(this.newTask.value);
    this.newTask.value = '';
  }

  handleKeyup(e) {
    this.props.handlekeyup(e.target.value);
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <input
          type="text"
          ref={(ref) => this.newTask = ref}
          onKeyUp={this.handleKeyup} placeholder="new task"
        />
        <button
          type="submit" disabled={!this.props.istext}
        >
          Add Task # {this.props.objects.length + 1}
        </button>
      </form>
    );
  }
}

サーバーに新しいタスクを送るメソッドなどをactions.jsに追加。

// ./client/src/actions.js
module.exports = {
  loadTasks,
  // 追加
  addTask,
  textExists,
};

// 追加
function addTask(newTask) {
  return (dispatch) => {
    dispatch(toggleLoading(true));
    $.ajax({
      url: '/api/tasks.json',
      type: 'POST',
      dataType: 'json',
      data: { name: newTask },
      success: (res) => {
        dispatch(newTaskAdded(res.id, res.name));
        dispatch(toggleLoading(false));
      },
      error: (xhr, status, err) => {
        dispatch(toggleLoading(false));
        console.log('/api/tasks.json', status, err.toString());
      },
    });
  };
}

function newTaskAdded(id, name) {
  return { type: 'TASK_ADDED', data: { id, name } };
}

function textExists(value) {
  return { type: 'TEXT_EXISTS', data: value };
}

アクションが渡された時の処理をindex.jsxのreducerに追加。

// ./client/src/index.jsx
case 'TASK_ADDED':
  return Object.assign({}, state, { tasks: state.tasks.concat(action.data) });
case 'TEXT_EXISTS':
  return Object.assign({}, state, { isText: action.data });

todo-appコンポーネントにtask-formコンポーネント、h3タグ、メソッドを追加

// ./client/src/components/todo-app.jsx
import TaskForm from './task-form.jsx';

class TodoApp extends React.Component {
  constructor(props) {
    super(props);
    // 追加
    this.handleNewTask = this.handleNewTask.bind(this);
    this.handleInputForm = this.handleInputForm.bind(this);
  }

  handleNewTask(task) {
    this.props.dispatch(actions.addTask(task));
  }

  handleInputForm(value) {
    this.props.dispatch(actions.textExists(value));
  }

  render() {
    return (
      <div>
        <h3>Todo List</h3>
        <TaskForm
          addtask={this.handleNewTask}
          handlekeyup={this.handleInputForm}
          objects={this.props.tasks}
          istext={this.props.isText}
        />
        {this.props.isLoading ...


  function mapStateToProps(state) {
    const { tasks, isLoading, isText } = state;
    return {
      tasks,
      isLoading,
      isText,
    };
  }

cssを追加しUIをRiot公式のTodoに近づける。

/* ./app/assets/stylesheets/application.css */
body {
 font-family: 'myriad pro', sans-serif;
 font-size: 20px;
 border: 0;
}

div#content {
  display: block;
  max-width: 600px;
  margin: 5% auto;
}

form input {
  font-size: 85%;
  padding: .4em;
  border: 1px solid #ccc;
  border-radius: 2px;
}

button {
  background-color: #1FADC5;
  border: 1px solid rgba(0,0,0,.2);
  font-size: 75%;
  color: #fff;
  padding: .4em 1.2em;
  border-radius: 2em;
  cursor: pointer;
  margin: 0 .23em;
  outline: none;
}

button[disabled] {
  background-color: #ddd;
  color: #aaa;
}

ul {
  padding: 0;
}

li {
  list-style-type: none;
  padding: .2em 0;
}

ブラウザでlocalhost:5000を更新して確認。
フォームにタスクの名前を入力しボタンを押して登録できること、更新後も登録したTodoが維持されていることを確認。

次回は完了したTodoをチェック出来るようにする。

React Redux Rails 4 / 5に続く

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へ続く

React Redux Rails 1 / 5 「環境構築」

はじめに

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

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

環境構築

  • React Redux Rails 1 / 5 「環境構築」

Todo Appの作成

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

参考

環境構築

Mac
Rails 5.0.0.1
ruby 2.3.1

ターミナル

$ rails new react_redux_rails
$ cd react_redux_rails
$ mkdir client
$ cd client

clientディレクト

Package.json

$ npm init -y

これによりpackage.jsonが作成されるので

{
  "private": "true",
  "scripts": {
    "webpack-watch": "webpack -w"
  }
}

のように書き換える。 再びターミナルで

$ npm install --save-dev webpack babel-core babel-loader babel-preset-react babel-preset-es2015

同じく

$ npm install --save react react-dom redux react-redux redux-thunk

webpackの設定

webpack.config.jsを作成し

// ./client/webpack.config.js
module.exports = {
  entry: {
    app: './src/index.jsx',
  },

  output: {
    path: '../app/assets/javascripts/webpack',
    filename: 'bundle.js',
  },

  module: {
    loaders: [
      { test: /\.(js|jsx)$/,
        loader: "babel",
        exclude: /node_modules/,
        query: {
          presets: ["es2015", "react"],
        }
      },
    ]
  },
};

そして

$ cd ..

で、アプリケーションのルートに戻っておく

Gemfile

Gemfileに

# Gemfile
gem 'foreman'

を追加し

$ bundle install

Procfile

アプリケーションのルートディレクトリにProcfileを作成

rails: rails s
webpack: npm --prefix client run webpack-watch

環境構築終わり。

次回からTodoアプリの作成。

React Redux Rails 2 / 5に続く