React とは
React は Facebook が開発している View 用の JavaScript ライブラリです。大きな特徴として仮想DOMと呼ばれるレンダリング機構がそなわっており、完成品のあるべきDOMの姿を記述することで、変更があった差分だけ DOM を追加/変更/削除してくれます。
仮想DOMはただの JavaScript の関数なので、React をそのまま利用しようとすると、DOM の記述を JavaScript の関数として書かないといけませんが、JSX を利用することで、非常にHTML に近しい形で DOM を記述することができます。
環境構築
前回の記事では、下記のコマンドを実行して、Redux および ES2015 の環境を揃えました。
# 前回のおさらい
mkdir src dist
npm init --yes
npm install -g browserify
npm install --save-dev babelify@7.2.0 babel-preset-es2015
npm install --save redux
echo '{"presets": ["es2015"]}' > .babelrc
今回は、前回のコマンドに加えて下記コマンドの実行をし、React 及びReact との連携に必要なライブラリ及びツールを揃えます。
# 今回新規に実行するコマンド
npm install --save-dev babel-preset-react
npm install --save react react-dom react-redux
echo '{"presets": ["react", "es2015"]}' > .babelrc # es2015 に加えて、react の preset を追加
それでは実装に入っていきましょう。
実装
前回、下記のように、Redux を使用して、ToDo リストを console に出力する
コードを書いたかと思います。
/* 前回のおさらい */
'use strict';
import { createStore } from "redux";
// ToDo を一意に特定できる ID
let nextTodoId = 0;
// ToDo の追加
function addToDo (text) {
return {
type: 'ADD_TODO',
id: nextTodoId++,
text: text,
};
}
// ToDo の完了/未完了
function toggleToDo (id) {
return {
type: 'TOGGLE_TODO',
id: id,
};
}
function reducer (state, action) {
switch (action.type) {
case 'ADD_TODO':
return {
todo: state.todo.concat({
id: action.id,
text: action.text,
completed: false
})
};
case 'TOGGLE_TODO':
return {
todo: state.todo.map(t => {
if (t.id !== action.id) {
return t;
}
return Object.assign({}, t, {completed: !t.completed});
})
};
default:
return state;
}
}
const store = createStore(reducer, {todo: []});
// Store に変更があれば、state を console に出力する
store.subscribe(() => console.log(store.getState()));
console.log("買い物を追加");
store.dispatch(addToDo("買い物に行く"));
console.log("銀行を追加");
store.dispatch(addToDo("銀行に行く"));
console.log("銀行に行くのをDone");
store.dispatch(toggleToDo(1)); こちらのコードに React のコードを追加して、
ToDo アプリケーションを作っていきたいと思います。
それでは ToDo アプリを表示するため、dist ディレクトリに index.html を追加します。
<!-- dist/index.html -->
<html>
<head>
<title>ToDoアプリ</title>
</head>
<body>
<div id="container"></div>
<script src="./app.js"></script>
</body>
</html> script タグで同 dist ディレクトリ内の app.js ファイルを読み込んでいます。
また、React のDOMの描画先として、div タグを作成しておきます。
それでは、React のコンポーネントを作成していきますが、その前に、前回作成した Redux 周りのコードのうち、console.log 出力している箇所は不要なので削除しておきます。
/* 削除 */
/*
// Store に変更があれば、state を console に出力する
store.subscribe(() => console.log(store.getState()));
console.log("買い物を追加");
store.dispatch(addToDo("買い物に行く"));
console.log("銀行を追加");
store.dispatch(addToDo("銀行に行く"));
console.log("銀行に行くのをDone");
store.dispatch(toggleToDo(1));
*/それではReact のコンポーネントを作成します。
import React from 'react';
const App = () => (
<div>
<AddToDoComponent />
</div>
)App コンポーネントを作成しました。これが ToDo アプリケーションを表すコンポーネントとなります。コンポーネントの中では、div タグで囲んだ AddToDoComponent を呼んでいます。js ファイル中に突然、HTMLタグが登場しましたが、このHTMLタグ部分が jsx となります。この jsx 部分は、最初の環境構築時にインストールした babel-preset-react によって、ビルド時に下記のようなコードに変換されます。
var App = function App() {
return React.createElement(
'div',
null,
React.createElement(AddToDoComponent, null)
);
};上記のコードを見てわかる通り、jsx 部分は、React の createElement を呼ぶただのJavaScript のコードであり、実体は createElement で生成された仮想DOMというわけです。
App コンポーネントの中に記述した AddToDoComponent についてはまだ未実装でした。それでは、ToDo を追加するフォームである AddToDoComponent を実装します。
import React from 'react';
import { Provider,connect } from 'react-redux'; // 追加
// 追加
let AddToDoComponent = ({dispatch}) => {
let input;
const onSubmit = (e) => {
e.preventDefault();
dispatch(addToDo(input.value));
input.value = "";
};
return(
<div>
<form onSubmit={onSubmit}>
<input ref={ node => {input = node} }/>
<button>Todo に追加する</button>
</form>
</div>
);
};
AddToDoComponent = connect()(AddToDoComponent); App コンポーネントと同様に、jsx で、AddToDoComponent を作成し、
react-redux を使って、Redux と連携を行っています。上から解説します。
import { Provider,connect } from 'react-redux'; // 追加react-redux から connect 関数と Provider コンポーネントをインポートしています。connect は Redux と React コンポーネントの接続を行う関数です。Provider はまた後ほど使用します。
let AddToDoComponent = ({dispatch}) => {
let input;
const onSubmit = (e) => {
e.preventDefault();
dispatch(addToDo(input.value));
input.value = "";
};AddToDoComponent を定義しています。引数で dispatch を受け取っています。後ほど説明しますが、AddToDoComponent は connect 関数でラップされて使用されます。connect 関数でラップされたコンポーネントは、引数として dispatch 関数を受け取ります。これは Store に紐付けられた Redux の dispatch 関数です。dispatch 関数は、onClick などの View 側の操作により、Store に対して Action を投げたい場合に使用します。
AddToDoComponent 内で、ToDo に追加するボタンが押下された時に実行される onSubmit 関数を定義します。onSubmit 関数の内部では、先ほど説明しました dispatch 関数を通じて、前回の Redux コードを作成した際に定義した addToDo Action を Store へ dispatch しています。
return(
<div>
<form onSubmit={onSubmit}>
<input ref={ node => {input = node} }/>
<button>Todo に追加する</button>
</form>
</div>
);最後の return 文にて、ToDo を追加するための、フォームの仮想DOMを作成します。
AddToDoComponent = connect()(AddToDoComponent);
作成した AddToDoComponent を、react-redux の connect 関数でラップしています。これで、AddToDoComponent は Redux と連携することができます。
これで、AddToDoComponent 及び App コンポーネントが作成できました。
これを HTML上にレンダリングします。
import { render } from 'react-dom'; // 追加
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('container')
)先ほど react-redux でインポートした Provider コンポーネントで、App コンポーネントをラップします。Provider の store プロパティに、前回作成した Redux の store を渡すことで、App 及びその中の AddToDoComponent が store にアクセスすることができるようになります。
そして、react-dom の render 関数によって、React コンポーネントを div タグの中に描画します。
ここまでで、ビルドすることで、タスクを追加するためのフォームが表示されたかと思います。
# ビルドコマンド(前回記事の再掲) browserify ./src/app.js -t babelify -o ./dist/app.js
ToDo へ追加するボタンを押下することで、onSubmit 関数を通じて、dispatch が実行され、Store に新しい ToDo が追加されますが、またToDo 一覧を表示していないので、
何も起こりません。
引き続き、ToDo 一覧である、ToDoListComponent を作成していきます。
let ToDoListComponent = ({todo}) => {
return (
<ul>{
todo.map( (t) => {
return <li key={t.id}>{ t.text }</li>;
})
}</ul>
);
};
function mapStateToProps(state) {
return {
todo: state.todo
};
}
ToDoListComponent = connect(mapStateToProps)(ToDoListComponent);先ほどの AddToDoComponent と同様にコンポーネントを作成したあとに、connect 関数を通じて、Redux と連携するコンポーネントを作成しています。
先ほどの AddToDoComponent と違い、ToDoListComponent では、mapStateToProps 関数を定義して、connect 関数の第一引数に指定しています。
mapStateToProps 関数の state には、Store が更新されるたびに、Store の情報が渡ってきます。それを、React のプロパティとして受け取れる形に変換するのが、mapStateToProps の役割です。
最後に ToDoListComponent を App コンポーネントから呼びます。
const App = () => (
<div>
<AddToDoComponent />
<ToDoListComponent />
</div>
)ビルドすると、タスクの一覧が表示されました。
それでは仕上げとして、このToDo を完了/未完了にできるようにしましょう。ToDoListComponent を修正します。
// 引数にて dispatch を受け取る
let ToDoListComponent = ({todo, dispatch}) => {
return (
<ul>{
todo.map( (t) => {
return <li key={t.id}>
{/* span タグと、checkbox を追加 */}
<span style={ { textDecoration: t.completed ? 'line-through' : 'none' } }>
{ t.text }
<input type="checkbox" onClick={ (e) => { dispatch(toggleToDo(t.id)) } } checked={ t.completed } />
</span>
</li>;
})
}</ul>
);
};チェックボックスの ON / OFF によって、タスクの状態を変更するため、AddToDoComponent と同様に dispatch 関数を受け取っています。
チェックボックスがクリックされたら、前回の Redux コードを作成した際に定義した toggleToDo Action を dispatch を通じて Store に投げています。Store の状態が変更されると、react-redux が変更を検知し、React コンポーネントの再描画が走ります。
これで ToDo が完成しました。
最終的にコードは以下のようになります。
'use strict';
import { createStore } from "redux";
import React from 'react';
import { render } from 'react-dom';
import { Provider,connect } from 'react-redux';
// ToDo を一意に特定できる ID
let nextTodoId = 0;
// ToDo の追加
function addToDo (text) {
return {
type: 'ADD_TODO',
id: nextTodoId++,
text: text,
};
}
// ToDo の完了/未完了
function toggleToDo (id) {
return {
type: 'TOGGLE_TODO',
id: id,
};
}
function reducer (state, action) {
switch (action.type) {
case 'ADD_TODO':
return {
todo: state.todo.concat({
id: action.id,
text: action.text,
completed: false
})
};
case 'TOGGLE_TODO':
return {
todo: state.todo.map(t => {
if (t.id !== action.id) {
return t;
}
return Object.assign({}, t, {completed: !t.completed});
})
};
default:
return state;
}
}
const store = createStore(reducer, {todo: []});
let AddToDoComponent = ({dispatch}) => {
let input;
const onSubmit = (e) => {
e.preventDefault();
dispatch(addToDo(input.value));
input.value = "";
};
return(
<div>
<form onSubmit={onSubmit}>
<input ref={ node => {input = node} }/>
<button>Todo に追加する</button>
</form>
</div>
);
};
AddToDoComponent = connect()(AddToDoComponent);
let ToDoListComponent = ({todo, dispatch}) => {
return (
<ul>{
todo.map( (t) => {
return <li key={t.id}>
<span style={ { textDecoration: t.completed ? 'line-through' : 'none' } }>
{ t.text }
{/* 追加 */}
<input type="checkbox" onClick={ (e) => { dispatch(toggleToDo(t.id)) } } checked={ t.completed } />
</span>
</li>;
})
}</ul>
);
};
function mapStateToProps(state) {
return {
todo: state.todo
};
}
ToDoListComponent = connect(mapStateToProps)(ToDoListComponent);
const App = () => (
<div>
<AddToDoComponent />
<ToDoListComponent />
</div>
)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('container')
);まとめ
ちょっと駆け足になってしまいましたが、Redux と React についていかがだったでしょうか。
大規模なアプリケーション開発において、アプリケーションの「状態」をどのように管理するかについて、Redux では、Action, Dispatcher, Store, View という4つの概念を用いて、情報のフローを明確にするアプローチを取っています。
このような簡単なToDo アプリケーションだと、最初に書くコードの記述量が増えてしまうのですが、コードの規模が大きくなればなるほど、コードの見通しがよくなり、のちのちの機能修正/改修の際の保守性が向上します。
ぜひ皆さんも、Redux と React を試してみてください。
さいの入門シリーズはコチラ