reactjs
redux

【react+redux】で楽観的に更新する

More than 1 year has passed since last update.

背景

http://qiita.com/halhide/items/a45c7a1d5f949596e17d

の続き。

※上の記事書いた時はreduxの1系はまだrcだったし、react-reduxも0.2とかそんなんだったのに、気がついたらrc取れてるは2系になってるとか動きが早すぎです。

今回は追加フォームの動きを実装してみた。

やりたいこと

追加ボタンを押した時点で見た目上のリストに反映し、裏で非同期での追加処理が終わったらサーバ側から取得したIDをstoreに反映させる。

実装

前回の記事と同様に、追加処理を見た目上の追加とサーバとの同期処理完了の二つのactionに分割します。非同期actionについては、前回の記事でも書いた通りredux-thunkを使って実現しています。

actions.jsx
export const ADD_TODO = 'ADD_TODO';
function addTodo(todo) {
    return {
        type: ADD_TODO,
        todo: todo
    };
}
export const SAVE_TODO = 'SAVE_TODO';
function saveTodo(todo) {
    return {
        type: SAVE_TODO,
        todo: todo
    };
}
export function createTodo(text) {
    var todo = {status: 0, text: text};
    return (dispatch, getState) => {
        dispatch(addTodo(todo));
        return $.post(
            'http://127.0.0.1:3000/todos',
            {text: text}
        ).then(data => {
            todo.id = data.todo.id;
            dispatch(saveTodo(todo));
        });
    }
}

componentに公開するアクションはcreateTodoですが、内部的には見た目上の追加アクションであるaddTodoと、サーバ側への追加完了アクションであるsaveTodoを起こすようになっています。

todoオブジェクトをreturnする関数の外側で定義して、二つのアクション内で共通のオブジェクトを参照するようにしています。理由としては、saveTodoで新たにオブジェクトを作って追加してしまうと、addTodoで見た目上追加したオブジェクトと表示がだぶってしまうためです。

reducers.jsx
    // switchの中だけ
    case ADD_TODO:
        var currentTodos = state.todos;
        currentTodos.push(action.todo);
        return assign({}, state, {todos: currentTodos});
    case SAVE_TODO:
        return assign({}, state, {todos: state.todos});

ADD_TODOでリストに追加して、UIに反映します。SAVE_TODOではtodosを設定し直しています。saveTodoで既存のオブジェクトにIDを反映済みなので、ここでは特にする事がありません。が、何もしないとstateが変わらずUIが変わらないので、stateを設定し直す事で再描画を走らせます。状態変更自体はreducerではなくアクションでやっているので、これで本当に良いのかなぁ感は否めないです。

この実装では、見た目上データをUIに反映するのは止めましょう、と仕様が変わった時点でsaveTodoが使えなくなります。saveTodoの引数でもらったtodoがリストになければ追加し、既にあれば上書きする、といった実装の方が仕様変更に強くなりそうです。さらに、状態変更をreducerでやることになるのもGoodです。Backboneのcollectionとか使えば、集合に同一要素がなけば追加、あれば更新みたいな集合操作の実装が簡単にできるので、合わせて使うとこの辺の実装は改善できそう。(めんどくさくてサボった)

todoform.jsx
export default class TodoForm extends React.Component {
  _onClick() {
      this.props.save(this.refs.inputText.getDOMNode().value);
  }

  render() {
      return (
        <div>
            <input ref="inputText" className="keyword" type="text" />
            <button onClick={this._onClick.bind(this)}>Add</button>
        </div>
      );
  }
}

あとは、formから呼ぶだけです。

まとめ

  • リモートとの同期が終わったタイミングで、見た目上追加したオブジェクトを更新する
  • 集合操作を簡単にできるライブラリと合わせて使った方がいろいろ楽そう