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 を試してみてください。
さいの入門シリーズはコチラ