Learn プログ

-マナビメモ-

React Redux Rails 4 / 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

参考

checkbox

チェックボックスを作成。

// ./client/src/components/task-list.jsx
export default class TaskList extends React.Component {
  constructor(props) {
    super(props);
    // 追加
    this.handleCheck = this.handleCheck.bind(this);
  }

  handleCheck(e) {
    this.props.handlecheck(e.target.id, e.target.checked);
  }

  render() {
    return (
      <ul>
        {this.props.tasks.map(task => (
          <li key={task.id}>
            <label className={task.isComplete ? "completed" : null}>
              <input
                type="checkbox"
                id={task.id}
                checked={task.isComplete}
                onChange={this.handleCheck}
              />
              {task.name}
            </label>
          </li>
        ))}
      </ul>
    );
  }
}

todo-app.jsxにマウントされているtask-listコンポーネント

// ./client/src/components/todo-app.jsx
class TodoApp extends React.Component {
  constructor(props) {
    super(props);
    this.handleNewTask = this.handleNewTask.bind(this);
    this.handleInputForm = this.handleInputForm.bind(this);
    // 追加
    this.handleTaskCompletionChange = this.handleTaskCompletionChange.bind(this);
  }

  // 追加
  handleTaskCompletionChange(id, isComplete) {
    this.props.dispatch(actions.toggleComplete(id, isComplete));
  }


    <TaskList
      tasks={this.props.tasks}
      // 追加
      handlecheck={this.handleTaskCompletionChange}
    />

サーバーにチェックの状態を送るメソッドなどをactions.jsに追加。

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

function toggleComplete(id, isComplete) {
  return (dispatch) => {
    $.ajax({
      url: `/api/tasks/${id}`,
      type: 'PATCH',
      dataType: 'json',
      data: { isComplete },
      success: (res) => {
        dispatch(completeChanged(res.id, res.isComplete));
      },
      error: (xhr, status, err) => {
        console.log(`/api/tasks/${id}`, status, err.toString());
      },
    });
  };
}

function completeChanged(id, isComplete) {
  return {
    type: 'TASK_COMPLETION_CHANGED',
    data: { id, isComplete },
  };
}

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

// ./client/src/index.jsx
case 'TASK_COMPLETION_CHANGED': {
  const taskIndex = state.tasks.findIndex((task) => {
    return task.id == action.data.id;
  });
  const newTasks = [
    ...state.tasks.slice(0, taskIndex),
    Object.assign({}, state.tasks[taskIndex], { isComplete: action.data.isComplete }),
    ...state.tasks.slice(taskIndex + 1),
  ];
  return Object.assign({}, state, { tasks: newTasks });
}

チェックした時のスタイルを追加。

/* ./app/assets/stylesheets/application.css */
.completed {
  text-decoration: line-through;
  color: #ccc;
}

label {
  cursor: pointer;
}

ここまででviewは完成。

Rails

Migration

サーバー側でisCompleteを扱えるようにする。 ターミナルでctrl-cでforemanを終了させてから

$ rails g migration addIsCompleteToTasks isComplete:boolean

できたマイグレーションファイルに , default: false を追加し

# ./db/migrate/日付_add_is_complete_to_tasks.rb
class AddIsCompleteToTasks < ActiveRecord::Migration[5.0]
  def change
    add_column :tasks, :isComplete, :boolean, default: false
  end
end

マイグレーションを反映させる。

$ rails db:migrate

これでデータベースのテーブルにisCompleteカラムが追加された。

Controller

次にcontrollerでupdateについての処理を書く。

# ./app/controllers/api/tasks_controller.rb
def update
  @task = Task.find(params[:id])
  @task.assign_attributes(task_params)
  if @task.save
    render :show, status: :ok
  else
    head :unprocessable_entity
  end
end

private

# isCompleteの追加
def task_params
  params.permit(:name, :isComplete)
end

View(JSON)

JSONがisCompleteも返すようにindex.json.jbuilderにisCompleteを追加。

# ./app/views/api/tasks/index.json.jbuilder
json.tasks(@tasks) { |t| json.extract! t, :id, :name, :isComplete }

同じく、JSONがisCompleteを返すようにshow.json.jbuilderにisCompleteを追加。

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

再びターミナルで

$ foreman start

でサーバーを開始。

ブラウザでlocalhost:5000を更新してチェックしてもエラーにならないことを確認。
また更新後チェックの状態が維持されることも確認。

Warning

タスクを新しく追加し、チェックの状態を変えると以下のような警告がでるが、解消できなかった。(実力不足!!!)
動作には問題なかったので諦めた。

Warning: TaskList is changing an uncontrolled input of type checkbox to be controlled.  
Input elements should not switch from uncontrolled to controlled (or vice versa).  
Decide between using a controlled or uncontrolled input element for the lifetime of the component.  
More info: https://fb.me/react-controlled-components

サーバーエラー時の処理

チェックの状態が変更された時にサーバー側でエラーが起きると、サーバー側とブラウザ側で矛盾が生じる(checkされているのにisCompleteがfalseまたはその逆)。それを防ぐために、エラー時にチェックを反転させる処理をactions.jsに書く。

// ./client/src/actions.js
function toggleComplete(id, isComplete) {
  return (dispatch) => {
    $.ajax({
      url: `/api/tasks/${id}`,
      type: 'PATCH',
      dataType: 'json',
      data: { isComplete },
      success: (res) => {
        dispatch(completeChanged(res.id, res.isComplete));
      },
      error: (xhr, status, err) => {
        // 追加
        dispatch(completeChanged(id, !isComplete));
        console.log(`/api/tasks/${id}`, status, err.toString());
      },
    });
  };
}

またこの時エラーが起こったことがわかりやすいように、エラーメッセージが表示されるようにする。
まずerror-messageコンポーネントを作成。

// ./client/src/components/error-message.jsx
import React from 'react';

export default class ErrorMessage extends React.Component {
  render() {
    const style = {
      marginTop: "5px",
      paddingLeft: "10px",
      color: "white",
      backgroundColor: "red",
    };

    return (
      <div style={style}>
        {this.props.message}
      </div>
    );
  }
}

todo-app.jsxでerror-messageコンポーネントを読み込みマウントさせる。

// ./client/src/components/todo-app.jsx
import ErrorMessage from './error-message.jsx';

<ErrorMessage message={"Text Message"} />
{this.props.isLoading

ブラウザでlocalhost:5000を更新して Text Message というメッセージが表示されていることを確認。
エラーメッセージが表示できるようになったので、エラーが起こった時にだけ表示されるようにする。
showErrorメソッドを作成し、サーバーのエラー時の処理に追加。

// ./client/src/action.js
error: (xhr, status, err) => {
  // 追加
  dispatch(showError('API Error'));
  dispatch(completeChanged(id, !isComplete));
  console.log(`/api/tasks/${id}`, status, err.toString());
}

function showError(message) {
  return { type: 'SHOW_ERROR', data: message };
}

dispatchメソッドでアクションが送られたときの処理をreducerに追加。

// ./client/src/index.js
case 'SHOW_ERROR':
  return Object.assign({}, state, { isError: true, errorMessage: action.data });

先程のように固定ではなく、stateの変化に対応できるよう書き換え。

//  ./client/src/components/todo-app.jsx
// 書き換え
{this.props.isError ? <ErrorMessage
  message={this.props.errorMessage}
/> : null}
{this.props.isLoading


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

ここでわざとサーバーのurlを間違えたものにしておき、エラーが起こるようにしておく。
ブラウザでlocalhost:5000を更新する。チェックの状態を変更するとエラーが起こりエラーメッセージが表示されること、チェックが反転されることを確認。
次にエラーメッセージが2秒後消えるようにする。

// ./client/src/action.js
error: (xhr, status, err) => {
  // 書き換え
  dispatch(tempErrorMessage('API Error'));
  dispatch(completeChanged(id, !isComplete));
  console.log(`/api/tasks/${id}`, status, err.toString());
},

// 追加
function hideError() {
  return { type: 'HIDE_ERROR' };
}

function tempErrorMessage(message) {
  return (dispatch) => {
    dispatch(showError(message));
    setTimeout(() => {
      dispatch(hideError());
    }, 2000);
  };
}

新たなアクションの処理を追加。

// ./client/src/index.jsx
case 'HIDE_ERROR':
  return Object.assign({}, state, { isError: false, errorMessage: '' });

ブラウザでlocalhost:5000を更新する。チェックの状態を変更するとエラーが起こりエラーメッセージが表示され2秒後に消えること、チェックが反転されることを確認。

一応loadTasks()やaddTask()などサーバーと通信している他のメソッドにも

error: (xhr, status, err) => {
  dispatch(toggleLoading(false));
  // 追加
  dispatch(tempErrorMessage('API Error'));

このように追加して、エラーメッセージが表示されるようにしておく。

次回はTodoを削除できるようにする。

React Redux Rails 5 / 5に続く