Immutable.jsとReact.js


React.jsと一緒に使うことで、immutableなオブジェクトを扱うことができ、パフォーマンスが向上する「facebook/immutable-js」について勉強をしてみたのでそのメモです。

👽 Immutable.jsとは

Immutable.jsはimmutableなデータ構造を扱うJavaScriptのライブラリです。

const { Map } = require('immutable')
const map1 = Map({ a: 1, b: 2, c: 3 })
// map1からbを変更した別のオブジェクトを生成
const map2 = map1.set('b', 50)
// map1のオブジェクトはそのまま
map1.get('b') // 2
// map2のオブジェクトはbが50となっている
map2.get('b') // 50

🏀 Immutable.jsをReactに導入

ReactにImmutable.jsを導入することで変更ロジックをモデルの中にまとめ、保守性を向上できます。

<PostEditor
post={this.props.post}
onChange={this.props.onChange}
/>
class PostEditor extends React.Component {
addTag(tag) {
this.props.onChange(this.props.post.addTag(tag))
}
removeTag(tag) {
this.props.onChange(this.props.post.removeTag(tag))
}
}
import { Record, List } from 'immutable'
const PostRecord = Record({title: '', content: '', tags: List() })
class Post extends PostRecord {
addTag(tag) {
return this.set('tags', this.tags.push(tag))
}
removeTag(tag) {
return this.set('tags', this.tags.filter(t => t !== tag))
}
}

🐝 TODOアプリケーションの開発

package.jsonの準備とライブラリ導入

React.js、Redux、Immutable.jsを使ってTODOアプリケーションを作る手順を紹介します。

mkdir todo && cd todo
yarn init
# メインのライブラリ
yarn add react react-dom redux react-redux immutable
# 開発用のライブラリ
yarn add --dev webpack babel-loader babel-preset-es2015 babel-preset-react

package.jsonにwebpack用の設定を追記します。

"script": {
"build": "webpack --debug"
}

webpack.config.js

webpack.config.jsにwebpackのconfigを準備します。

module.exports = {
entry: './src/app.js',
output: {
path: __dirname,
filename: 'bundle.js'
},
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel',
query: { presets: [ 'es2015', 'react' ] },
}
]
},
resolveLoader: {
moduleExtensions: ['-loader']
}
};

index.html

プロジェクト直下に確認用のindex.htmlを作成します。

<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="style.css">
<title>Immutable Todo</title>
</head>
<body>
<div id="app"></div>
<script src="bundle.js"></script>
</body>
</html>

app.js

エントリポイントの./src/app.jsProviderによってstoreを認識させます。

import React from 'react';
import { render } from 'react-dom';
import { createStore } from 'redux'
import { Provider } from 'react-redux';
import reducer from './reducer';
import { TodoList } from './containers';
const store = createStore(reducer);
render(
<Provider store={store}>
<TodoList />
</Provider>,
document.getElementById('app')
);

actions.js

アクションを管理する./src/actions.jsを作成します。

// succinct hack for generating passable unique ids
const uid = () => Math.random().toString(34).slice(2);
export function addTodo(text) {
return {
type: 'ADD_TODO',
payload: {
id: uid(),
isDone: false,
text: text
}
};
}
export function toggleTodo(id) {
return {
type: 'TOGGLE_TODO',
payload: id
}
}

reducer.js

アクションと現在のstateから新しいstateを生成する./src/reducer.jsを作成します。

import { List, Map } from 'immutable';
const init = List([]);
export default function(todos=init, action) {
switch(action.type) {
case 'ADD_TODO':
return todos.push(Map(action.payload));
case 'TOGGLE_TODO':
return todos.map(t => {
if(t.get('id') === action.payload) {
return t.update('isDone', isDone => !isDone);
} else {
return t;
}
});
default:
return todos;
}
}

components.js

実際のコンポーネントを./src/components.jsに作成します。コンポーネントにはTodoListTodoが存在します。

import React from 'react';
export function Todo(props) {
const { todo } = props;
if(todo.isDone) {
return <del>{todo.text}</del>;
} else {
return <span>{todo.text}</span>;
}
}
export function TodoList(props) {
const { todos, toggleTodo, addTodo } = props;
const onSubmit = (event) => {
const input = event.target;
const text = input.value;
const isEnterKey = (event.which == 13);
const isLongEnough = text.length > 0;
if(isEnterKey && isLongEnough) {
input.value = '';
addTodo(text);
}
};
const toggleClick = id => event => toggleTodo(id);
return (
<div className='todo'>
<input type='text'
className='todo__entry'
placeholder='Add todo'
onKeyDown={onSubmit} />
<ul className='todo__list'>
{todos.map(t => (
<li key={t.get('id')}
className='todo__item'
onClick={toggleClick(t.get('id'))}>
<Todo todo={t.toJS()} />
</li>
))}
</ul>
</div>
);
}

container.js

コンポーネントがstoreを認識するために、コンポーネントをラップするコンテナ./src/container.jsを作成します。

import { connect } from 'react-redux';
import * as components from './components';
import { addTodo, toggleTodo } from './actions';
export const TodoList = connect(
// <TodoList todos={state} /> と同義
function mapStateToProps(state) {
return { todos: state };
},
// 以下と同義
// <TodoList todos={state}
// addTodo={text => dispatch(addTodo(text))}
// toggleTodo={id => dispatch(toggleTodo(id))} />
function mapDispatchToProps(dispatch) {
return {
addTodo: text => dispatch(addTodo(text)),
toggleTodo: id => dispatch(toggleTodo(id))
};
}
)(components.TodoList);

🎉 参照リンク

📚 おすすめの書籍