React + Redux 入門
Introduction
React・Reduxの基本的な仕組み・使い方ついて解説します。
Attention
自分もまだ使い始めたばかりなので、
間違っているところなどあればご指摘お願いします :bow:
React
A JavaScript library for building user interfaces
- あくまでViewを作るためのライブラリ
- Angularのようなフレームワークではない
- Routingなどは別途実装する必要がある
- Angular, Vue.jsなどのフレームワークとも組み合わせ可能
- ex. Viewの所だけReactにする
https://facebook.github.io/react/
Pros
- VirtualDOMという仕組みによりUnitTestを書くことが容易になる
- Debugもしやすくなります
- Componentを組み合わせることで複雑なUIを簡単に構築可能
- また、Componentはアプリケーション内で再利用可能
- 他のJSライブラリと組み合わせることが出来る
- ex. Angular + React
- パフォーマンス向上が望める
- Angular, VueJS 辺りもVirtualDOM採用している筈なので差は縮まってきているかも?
Cons
- 比較的学習コストが高い
- 個人的にはAngularの方がやってること多いので難しい気が...
- 依存ツールが多い
- Babel, Webpack, CommonJS, ...
- 記述が冗長になりがち
- フルスタックなフレームワークではない
- View以外の必要な処理は別途実装する必要がある
- デザイナーとの役割分担が難しい
- JSXを覚えてもらえれば何とか...
Features
Declarative
あるべきViewを宣言するように記述できる
Component-Based
Componentを組み合わせることでViewを構築
Learn Once, Write Anywhere
Rectを覚えれば様々な環境で使用可能 (ex. React Native)
Declarative
命令的
- :scream: JSだけで完結せず、様々なものにViewが依存
- :scream: テスト・デバッグが困難
$('button').on('click', () => { $('h1').text('hello'); });
宣言的
- :smile: Viewがstateのみに依存
- :smile: テスト・デバッグが容易
render() { return ( <div> <h1>{this.state.title}</h1> <button onClick={() => { this.setState({ title: 'hello' }); }}> Click </button> </div> ); }
Component-Based
ReactではComponentを組み合わせてViewを構築します。
これにより、複雑なUIを簡単に作ることが可能となります。
複数Component組み合わせイメージ
const Header = () => ( <header> // This is header... </header> ); const SearchForm = () => ( <form> // This is search form... </form> ); const MainContent = () => ( <div> <LeftNav /> // 左ナビComponent <TopMenu /> // メニューComponent <Ad /> // 広告Component </div> ); class App extends Component { render() { return ( <div> <Header /> // ヘッダーComponent <SearchForm /> // 検索フォームComponent <MainContent /> // メインComponent </div> ); } }
※ 実際にReactは使われていません
Reactを使ってみよう!
Hello World
render
関数に表示したいElementと、表示先のDOMを指定すればOK!
ReactDOM.render( <h1>Hello World!!</h1>, document.getElementById('root') );
この様なHTMLが出力されるはずです。
<body> <div id="root"> <h1 data-reactroot="">Hello World!!</h1> </div> </body>
CodePen
今すぐにReactを動かしてみたい方はこちらからどうぞ
既に実行できる環境が整っています!
http://codepen.io/gaearon/pen/ZpvBNJ?editors=0010
DOM描画
ReactではComponentを組み合わせてUIを構築していきます。
このComponentを元にインスタンスであるElementを生成し、
これらのVirtualDOMを通して実際のDOMに描画していきます。
// Component定義 var Hello = function Hello() { return React.createElement( "h1", null, "Hello World!!" ); }; // Element生成 & 描画 ReactDOM.render( React.createElement(Hello, null), document.getElementById('root') ); // JSX ver. const Hello = () => ( <h1>Hello World!!</h1> ); ReactDOM.render( <Hello />, document.getElementById('root') );
JSX
Elementを生成する、HTML風の独自syntax (※ HTMLではないです)
JSXを使うことにより面倒なElement作成を簡単に行うことができます。
公式でも利用が推奨されているので、積極的に使っていきましょう!
<h1 id="test" hoge={false}>Hello World!!</h1> // JSX
React.createElement( "h1", { id: "test", hoge: false }, "Hello World!!" );
Babel Online Compiler
リアルタイムでBabelによるトランスパイルを行ってくれる環境があるので、
今すぐJSXを試してみたい方はこちらからどうぞ!
Props & State
ReactではProps・Stateと呼ばれるデータによって状態を管理していきます。
Props | State | |
---|---|---|
データの更新 | NG | OK |
定義元 | 親Component | 自Commponent |
イメージ | Constructorから渡された引数 | Privateなインスタンス変数 |
Props
Componentは外部からデータを受け取ることが可能となっていて、
その入力データの事をPropsと呼びます。
受け取ったPropsの値に応じて表示する内容を変化させたり、
クリック時のCallback等にも使用することができます。
class Welcome extends Component { render() { // propsを元にViewを宣言 return <h1>Hello, {this.props.name}</h1>; } } class App extends Component { render() { // props指定 return <Welcome name="Tom">; } }
ただし、同じPropsが渡されたら同じViewが表示されるべきであるため、
値を更新しない ように注意してください。
State
Propsは親コンポーネントなどの外部から渡されるデータでしたが、
StateはComponent自身が保持しているローカルなデータになります。
また、Propsとは異なりデータの更新が可能となっています。
ただし、更新を行う際は必ずsetState
関数を使用してください。
直接値を更新してしまうと更新時のイベントが呼ばれずViewが再描画されなくなります。:scream:
class Counter extends React.Component { constructor() { super(); this.state = { count: 0 }; // stateの初期値を定義 } render() { return ( // クリックしたらインクリメント // ※ this.state.count++だと元の値を書き換えるのでNG <button onClick={() => { this.setState({ count: this.state.count + 1 }); }} > {this.state.count} </button> ); } }
Lifecycle
ComponentにはLifecycle関連のメソッドが幾つか用意されていますが、
それらがどの様に呼ばれていくのか確認していきたいと思います。
Mounting
constructor()
- state, propsなどの初期化処理
componentWillMount()
render()
- DOM描画
- 単一のElementを返す必要がある
null
,false
等を返した場合は何も描画されない
componentDidMount()
- mountされた後に1回だけ呼ばれる
- Ajaxでの初期データ読み込みや、描画したDOMへのイベント追加に使える
Updating
componentWillReceiveProps(nextProps)
- Propsが更新されたときに呼ばれる
- 変更後のPropsに応じてStateも変更するときなどに使用可能
shouldComponentUpdate(nextProps, nextState)
- 返り値によって再描画する・しないを制御可能
true
: 描画する、false
: 描画しない
componentnWillUpdate(nextProps, nextState)
- DOMの更新前に呼ばれる
- ここではStateの更新ができない
render()
componentDidUpdate(prevProps, prevState)
- DOMが更新された後に呼ばれる
- DOMの更新に応じて処理をしたい場合などに使用可能
Unmounting
componentWillUnmount()
- unmountされる前に呼ばれる
- Component内で使用していたDOMイベントなどを解除するのに使用可能
Event Handler
"ボタンをクリックしたとき"・"フォームをSubmitした時"等の
イベント処理に関して見ていきたいと思います。
クリックしたら何かしてみる
下記のようにonClick
のイベントハンドラーを渡すだけで、
クリック時の挙動を制御することが可能となります。
const Hello = () => ( <button onClick={() => { alert('Hello!!'); }}> Click Me </button> );
サポートされているイベント一覧
- Clipboard Events
- Composition Events
- Keyboard Events
- Focus Events
- Form Events
- Mouse Events
- Selection Events
- Touch Events
- UI Events
- Wheel Events
- Media Events
- Image Events
- Animation Events
- Transition Events
※ https://facebook.github.io/react/docs/events.html
Unit Test
公式からUnitTest用のサポートツールが提供されていますが、
今回はairbnbから提供されているenzymeを使ったテストを紹介したいと思います。
https://github.com/airbnb/enzyme
import React from 'react'; import { shallow, mount } from 'enzyme'; import { expect } from 'chai'; import sinon from 'sinon'; class Counter extends React.Component { constructor(props) { super(props); this.state = { count: props.initCount }; // stateの初期値を定義 } render() { return ( // クリックしたらインクリメント // ※ this.state.count++だと元の値を書き換えるのでNG <button onClick={() => { this.setState({ count: this.state.count + 1 }); }} > {this.state.count} </button> ); } } const App = ({ onClick }) => ( <div> <h1>Hello World!!</h1> <div className="container"> <Counter initCount={1} /> </div> <button className="btn" onClick={onClick}> Click Me </button> </div> ); describe('<App />', () => { it('タイトルにHello Worldが表示される', () => { // タグ名で検索 const wrapper = shallow(<App />); expect(wrapper.find('h1')).to.have.length(1); expect(wrapper.find('h1').text()).to.equal('Hello World!!'); }); it('containerクラスが描画される', () => { // タグのクラス名で検索 const wrapper = shallow(<App />); expect(wrapper.find('.container')).to.have.length(1); }); it('Counterが描画される', () => { // Containerで検索 const wrapper = shallow(<App />); expect(wrapper.find(Counter)).to.have.length(1); }); it('Counterの初期値は1に設定される', () => { // Containerで検索 const wrapper = shallow(<App />); expect( wrapper.find('Counter').prop('initCount') ).to.equal(1); }); it('.container内にbuttonが描画される', () => { // Full DOM Rendering であれば依存Componentの内部まで認識可能 const mountWrapper = mount(<App />); expect( mountWrapper.find('.container button') ).to.have.length(1); // Shallow Rendering した場合はCounterの中身を認識できない const shallowWrapper = shallow(<App />); expect( shallowWrapper.find('.container button') ).to.have.length(1); // 通らない }); it('ボタンをクリックしたらCallbackが呼ばれる', () => { // クリックイベントをシミュレーション const onClick = sinon.spy(); const wrapper = shallow(<App onClick={onClick} />); wrapper.find('.btn').simulate('click'); expect(onClick.calledOnce).to.be.true; }); });
Redux
ReactではStateにより状態を管理することが可能となっています。
ですが、各Componentでバラバラに状態管理してしまうと、
アプリケーションが巨大になるに連れてあっという間に複雑化してしまいます。
では、 単一の親コンポーネントで全ての状態を管理してみる のはどうでしょうか?
良さそうに見えますが、一箇所にロジックが集中してしまい、いずれは管理しきれなくなりそうです :scream:
※ https://html5experts.jp/koba04/20839/
そこで、この様な問題に対処するため、
アプリケーション内のデータフローを一方向のみで扱う Fluxと呼ばれる
新しいアーキテクチャが考案されました。
Fluxから派生して作られた新たなアーキテクチャにReduxと呼ばれるものがあります。
Fluxでは複数のStoreを持つことを許容していましたが、
Reduxにおいては単一のStoreで全ての状態を管理していきます。
※ https://html5experts.jp/koba04/20839/
Three Principles
Single source of truth
The state of your whole application is stored in an object tree within a single store.
State is read-only
The only way to change the state is to emit an action, an object describing what happened.
Changes are made with pure functions
To specify how the state tree is transformed by actions, you write pure reducers.
構成
- Actions
- Action Creators
- Reducers
- Store
Reduxにおいて全ての状態はStoreと呼ばれる単一のStateで管理されています。
この、 Storeを更新する際は必ずActionを発行 し、
Reducerと呼ばれる純粋な関数 によって新しいStateを生成する必要があります。
また、 Actionを発行する役割を持っているのがAction Creator となります。
※ https://html5experts.jp/koba04/20839/
Actions
ActionはStoreを更新する際に必要となる情報であり、Storeから見た情報源は全てここに集約されています。
下記が簡単なActionの例です。
{ type: 'ADD_TODO', text: 'Do something...' }
Actionは純粋なJavaScriptのObjectであり、type
プロパティを持っている必要があります。
この、type
に応じてどの様な処理を行うべきかを判断します。
Action Creators
Action CreatorはActionを生成するただの関数です。
function addTodo() { return { type: 'ADD_TODO', text: 'Do something...' }; }
Reducer
Actionからはどの様な行動が発生したのか認識できますが、どの様にStateを変更すべきかは明示していません。
この役割を担うのがReducerとなります。
Reducerは純粋な関数であり、現在のStateとActionから新たなStateを作成します。
function todoApp(state = initialState, action) { switch (action.type) { case 'ADD_TODO': return Object.assign({}, state, { todos: [...state.todos, action.text] }); default: return state; } }
また、Reducer内で次のような処理を含めてはいけません。
- 引数を編集する
- API呼び出しやページ遷移など、副作用のある処理
Date.now()
、Math.random()
などpureでない関数を呼ぶ
同じ引数が与えられたら、同じStateが返ってくるべきです。
そのためには、引数の編集や副作用のある処理は行わない必要があります。
Store
Storeは次の役割を担っています。
- アプリケーションのStateを保持
- getState()
経由でのアクセスを許可
- dispatch(action)
経由での更新を許可
- subscribe(listener)
経由でlistenerを登録
- subscribe(listener)
からreturnされたfunction経由でlistenerを解除
アプリケーション内のStoreはただ一つです。
データの扱いを分割したい場合は複数のStoreを作成するのではなく、reducerを組み合わせてください。
Storeを作成するのは簡単で、下記のようなコードで実現することができます。
import { createStore } from 'redux'; import todoApp from './reducers'; const store = createStore(todoApp);
Dispatching Actions
Storeを更新したい場合にはstore.dispatch()
にActionを渡せばOKです。
現在のStateと渡されたActionを元にReducerが新しいStateを作成します。
import { createStore } from 'redux'; import todoApp from './reducers'; import { addTodo } from './actions'; const store = createStore(todoApp); store.dispatch(addTodo('Learn about Redux'));
Redux with React
React用のRedux実装ライブラリとしてreact-redux
が提供されています。
$ npm install react-redux --save
Presentational and Container Components
Presentational Components | Container Components | |
---|---|---|
目的 | 見た目を定義 (markup, styles) | ビジネスロジックを定義 (data fetching, state updates) |
Redux依存 | 依存しない | 依存する |
データの参照方法 | props経由で参照 | Redux Stateを参照 |
データの更新方法 | propsのCallbackを呼び出す | ActionをDispatch |