Immutable.jsでreact+redux環境が楽になりそうな話

20160902200209

この記事はQiitaのReact Advent Calendar 2016/14日に投稿させていただく記事です。
お粗末な文章ですが失礼致します。

さて、みなさんreact+reduxしていますか?
僕は毎日react+reduxでハッスルしています。

まだまだreact+reduxを触りだして間もないのですが、Immutable.jsなるものを導入するとreact+redux環境が劇的に楽になるとの情報をキャッチしましたので、今回はその件について書かせて頂ければと思います。

Immutable.jsとは何ぞや?

reactと同じで天下のfacebook様が開発されているjsライブラリです。


「Immutable」と名前の通り、Immutable.jsではデータを不変なデータとして管理できるようになるそうです。

。。。なんのこっちゃ?っと思いましたので、基本的な使用方法を見ていきたいと思います。

基本的な使用方法


こちらの記事が非常にわかりやすく参考になりました。

stateの更新を行う際、redux?のお作法でstate自身を更新するのではなく、新しいstateを作成しないといけません。
そのため、簡単な更新でもこのような処理になってしまします。

const todos = (state=[],action) => {
  switch (action.type) {
    case "ADD_TODO":
      return [   //この部分
        ...state,
        {text:action.text}
      ]
    default:
      return state;
  }
}
export default todos

…stateで現在のstateを展開し、その後ろに追加するtodoのtextを挿入し、新しい配列をstateとして登録するといった感じですね。
このような簡単な処理だけで見ても、直感的でないことがわかります。
stateが階層構造にでもなったならば、容易に複雑になることが想定されます。。。

つらい(´・ω・`)

そんな時にImmutable.jsです!
Immutable.jsは上にも記述したように不変なデータを扱うことが可能です。
先程のtodoReducerをImmutable.jsを使用し記述するとこのようになります。

import { List } from 'immutable'
const init = List([])
const todos = (state=init,action) => {
  switch (action.type) {
    case "ADD_TODO":
      return state.push(action.text)
    default:
      return state
  }
}
export default todo

素晴らしいですね。
state.push(action.text)で元のstateは変更せず、新しくstateを作成し返却といった処理が感覚的に記述できるようになります。

ちょっと発展した使い方


以下で作成するアプリケーションはこちらのリポジトリのimmtable-projectに格納されています。
記事読むの面倒くさい!って方や動きを見ながら記事読みたいって方はご活用下さい。

npm install
npm start

で実行可能です。

発展的な使用方法でRecord関数を使用することでこういった書き方もできるようになります。

import { List,Record } from 'immutable'

const ToDoRecord = Record({
  text:''
})
const init = List([])
const todos = (state=init,action) => {
  switch (action.type) {
    case "ADD_TODO":
      return state.push(new ToDoRecord({text:action.text}))
    default:
      return state
  }
}
export default todo

今まで散文していた、エンティティ定義がToDoRecord にまとめられ一目でわかるようになります。
(stateのエンティティ定義はコメントで書かなくて良くなりそうです。)

さらにさらに、recordは継承することが可能でありメソッドを記述することが可能です。
IDを振るといった処理をrecordを継承し作成したクラスに定義したものが以下になります。

import { List,Record,Seq } from 'immutable'

const ToDoRecord = Record({
  todoList:List()
})

const ToDoContentRecord = Record({
  id:null,
  text:''
})

class ToDo extends ToDoRecord{
  getNextId(){
    let newNumber = 1;
    if(this.todoList.size != 0){
      newNumber = Seq(this.todoList).map(x => x.get('id')).max() + 1;
    }
    return newNumber;
  }
}

const todo = (state=new ToDo,action) => {
  switch (action.type) {
    case "ADD_TODO":
      return state.set('todoList',state.todoList.push(new ToDoContentRecord({
        id:state.getNextId(),
        text:action.text
      })))
    default:
      return state
  }
}

export default todo

ちょっと複雑になりますが、親レコード(ToDoRecord)で子レコード(ToDoContentRecord)を管理するようにし、ToDoRecordを継承したToDoクラス内でid連番処理を実装しています。
で、reducer部分ではstate.getNextIDを呼び出し、新しく算出したIDとtextを登録する処理を実装しています。
命名とかが汚いのはご容赦下さい。(´・ω・`)

ここまで来るとさらに欲が出てくると思います。
「これ、reducer部分さらに薄くできんじゃね?」

はい、実装してみます。

import { List,Record,Seq } from 'immutable'

const ToDoRecord = Record({
  todoList:List()
})

const ToDoContentRecord = Record({
  id:null,
  text:''
})

class ToDo extends ToDoRecord{
  addToDo(text){
    return this.set('todoList',this.todoList.push(new ToDoContentRecord({
      id:this.getNextId(),
      text:text
    })))
  }

  getNextId(){
    let newNumber = 1;
    if(this.todoList.size != 0){
      newNumber = Seq(this.todoList).map(x => x.get('id')).max() + 1;
    }
    return newNumber;
  }
}
export default ToDo
import ToDo from '../models/ToDo'

const todo = (state=new ToDo,action) => {
  switch (action.type) {
    case "ADD_TODO":
      return state.addToDo(action.text)
    default:
      return state
  }
}
export default todo

新しくビジネスロジックを記述するmodelsを作成し、そちらに寄せることでreducer部分がかなり薄くなります!
今までビジネスロジックをactionに記述するかreducerに記述するか迷っていましたが、modelsにビジネスロジックを分離することが可能です。
これは嬉しいですね。スッキリです!

さらにさらにさらに、欲を出してみましょう!
immutable.jsでRecordを継承することで、メソッドを定義することができました。
そのメソッドをactionを通じてreducerへ渡して上げれば、肥大化するaction,reducerをさらに薄く出来るのではないかと。。。

実装してみます。
先程実装したmodelsはそのままで、reducer、action、container、componentを以下のように実装します。

import React from 'react'

export default class ToDoComponent extends React.Component {
  render(){
    return(
      <div>
        <input type="text" ref="inputtodo" />
        <button onClick={() => {
          this.props.addToDo(this.props.todo.addToDo(this.refs.inputtodo.value))
        }}>submit</button>
      {this.props.todo.todoList.map(todo => (
          <p >{todo.get('text')}</p>
        ))}
      </div>
    )
  }
}
import { connect } from 'react-redux'
import ToDoComponent from '../components/ToDoComponent'
import { onChange } from '../actions/todo'

const mapStateToProps = (state) =>{
  return{
    todo:state.todo
  }
}

const mapDispatchToProps = (dispatch) =>{
  return {
    addToDo: (entry) => {
      dispatch(onChange(entry))
    }
  }
}

const ToDo = connect(
  mapStateToProps,
  mapDispatchToProps
)(ToDoComponent)

export default ToDo

this.props.addToDo(this.props.todo.addToDo(this.refs.inputtodo.value))でmodel内のaddToDo()を呼び出し、

addToDo: (entry) => {
  dispatch(onChange(entry))
}

でそのaddToDoメソッドをactionに渡してあげます。
あとはいつもどおりactionとreducerを作成してあげれば完成です。

export const onChange = (entry) =>{
  return {
    type:"ON_CHANGE",
    entry:entry,
  }
}
import ToDo from '../models/ToDo'

const todo = (state=new ToDo,action) => {
  switch (action.type) {
    case "ON_CHANGE":
      return action.entry
    default:
      return state
  }
}

export default todo

何か新しくstate更新処理を実装したくなった際は、modelとcontainer,component部分を編集するだけで実装することが可能になります。

もう、素晴らしすぎますね。
これで「あれ、あのactionどこだったっけ?」とか「actionのフォルダ階層深すぎワロタ」問題を解消することが可能になります。

弊害

弊害というほどのことではありませんが、Immutable.jsを使用するとstateが今までのオブジェクト構造ではなく、Immutable.jsでラップされたオブジェクトとなってしまうため、注意が必要です。
Http request処理部分やlocalstorageへのstate保存処理などを行う際はtoJS()でオブジェクトへ変換、取得する際はfromJS()でImmutable.jsのオブジェクトでラッピングを行うという点を覚えておけば大きなハマりはないと思います。

おわりに

いかかでしたでしょうか?
react+redux環境で問題としてあげられる、「action、reducer部分の肥大化問題」、ならびに「ビジネスロジックどこに書く問題」等々がImmutable.jsで解決できたと思います。
まだまだ、勉強したてですので、他にも弊害が出てくるかもしれませんが、僕の中では一旦この構成で開発を進めて行こうと思います。

就職活動

ただいまニートのGakuはおもしろい就職先を探しております。
大小問いませんし、IT企業でなくても構いません。

少しでも興味を持っていただけたならば、TwitterのDM等で連絡いただければ即対応させて頂きます。
Twitter:https://twitter.com/gaku3601

参考文献

React使い必見! Immutable.jsでReactはもっと良くなる

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です