Reactを使って何かアプリケーションを作ってみよう!そう思って勉強していると、最初はよくわからなかったreactもなんとなく分かってきますよね。そして、次にやってみようと思うのがreduxを使ったアプリケーションの作成。 まあ、React少しは分かってきたしReduxもいけるっしょ!と思いつつ資料を漁ってみると思う事がある。
Redux全然わからねえ・・・
Reactを初めて見たときも圧倒的分からなさだった気がするけど、Reduxを使って実装されたコードを見てみるとそれが何をやっているのかマジで分からん。これはやべえ。。
自分もReactでなんとなくアプリケーションを作って、Reduxやってみようかなあ、、と思って見てみたところ果てしなくわからない。。。今回は、Reduxをちょっとでも分かるようになれるように簡単なものを作って頑張って理解したい。
かなり長くなってしまったけど、まあいってみよう。徐々にReduxのコードにしていくので、ちょっと回りくどいかも。
まずはReactでアプリケーションを作る
まずはreactで簡単なものを作り、それをredux化していきたいと思う。formに値を入力し、送信ボタンを押したらそれがform下に表示されるようなものをreactで作る。
index.js
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import App from './App'; ReactDOM.render(<App />, document.querySelector('.container'));
App.jsx
import React, { Component } from 'react'; import Form from './Form'; class App extends Component { render() { return ( <Form /> ); } } export default App;
Form.jsx
import React, { Component } from 'react'; class Form extends Component { constructor(props) { super(props); this.state = { message: "", messages: [], } } handleChange(e) { this.setState({ message: e.target.value, }); } handleSubmit(e) { e.preventDefault(); const { messages } = this.state || []; messages.push(this.state.message); this.setState({ messages, message: '', }); } render() { return ( <div> <form> <input type="text" value={this.state.message} onChange={e => this.handleChange(e)} /> <input onClick={e => this.handleSubmit(e)} type="submit"/> </form> <div class="content"> <ul> { this.state.messages.map((message) => ( <li>{ message }</li> )) } </ul> </div> </div> ); } } export default Form;
上記のコードを実行するとこんな感じになる。やっていることは、送信したら文字をstateで管理しているmessagesという配列に入れて、それをform下に表示しているだけ。今はReactのみで書いたけど、これをReduxを使って実装していきたい。
reduxインストール
yarn add redux react-redux
storeとreducerの作成
reduxはアクションを発行してreducerを通してstoreの更新を行う。例えば、ユーザーが何か文字を入力してstateに更新を加えたいなら、その時にactionを発行し、reducerを経由しstateを持っているstoreが更新される。そのサイクルを繰り返してアプリケーションを動かしていくのがredux。
とは言っても、なかなかわかりづらいので実際にstoreとreducerを作っていこう。
storeを作る
index.jsに以下の記述をする。
import { createStore } from 'redux' const store = createStore(reducer);
reduxからcreateStoreという関数を引っ張り出しその引数にreducerを入れることでstoreを作成する事ができる。
reducerを作る
新たにreducersというディレクトリを作り、その中のindex.jsxに以下の記述を書く。
export default (state={}, action) => { return state; }
記述されている通り、stateとactionを受け取って新たなstateを返します。本当にstateがここで管理されて返ってくるのかを確かめてみよう。storeにはgetState()
という関数があり、これがstateを返してくれる。
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { createStore } from 'redux' import App from './App'; import reducer from './reducers/'; const store = createStore(reducer); const state = store.getState(); console.log(state); ReactDOM.render( <App />, document.querySelector('.container') );
export default (state={message: 'こんにちは'}, action) => { return state; }
reducerではデフォルトのstateを記述してしまい、これがgetState()
で返ってくるか確認する。{message: "こんにちは"}
という文字がブラウザのコンソールで確認することができるはず。
storeが更新された時に描画したい
さっきので、storeにあるstateを取ってくる事ができることは分かった。次に、このstateが更新されたら画面を再描画したい。
少しreactの描画の流れを思い出してみると、
- 何か値の変更を受け付ける
- setStateでstateが変更される
- 再度レンダリングされる
というものだったはず。
このサイクルを繰り返していくことによって、アプリケーションが動くのがreact。reduxを使った場合でも、stateが変更されたら再度レンダリングするという流れは変わらん。これを実現するためにstoreのsubscribe
という関数を使う。
index.js
const render = () => { const state = store.getState(); console.log(state); ReactDOM.render( <App />, document.querySelector('.container') ); } render(); store.subscribe(render);
さっきまで記述していたReactDOM.renderを関数にし、それをstoreのsubscrbe
という関数の引数にする。こうすることによって、storeに変更があった場合、新しいstateを反映したものがレンダリングされる。今回はrender関数の中身が再描画。
stateを取得してviewに表示する
stateを取得することができたので、これをviewに表示してみる。これは普通にreactでやったのと同じようにpropsで渡していくだけ。このstateをformのplaceholderに表示したいと思います。
index.js
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { createStore } from 'redux' import App from './App'; import reducer from './reducers/'; const store = createStore(reducer); const render = () => { const state = store.getState(); console.log(state); ReactDOM.render( <App message={state.message} />, document.querySelector('.container') ); } render(); store.subscribe(render);
App.jsx
import React, { Component } from 'react'; import Form from './Form'; class App extends Component { render() { return ( <Form message={this.props.message} /> ); } } export default App;
Form.jsx
import React, { Component } from 'react'; class Form extends Component { constructor(props) { super(props); this.state = { message: "", messages: [], } } handleChange(e) { this.setState({ message: e.target.value, }); } handleSubmit(e) { e.preventDefault(); const { messages } = this.state || []; messages.push(this.state.message); this.setState({ messages, message: '', }); } render() { return ( <div> <form> <input placeholder={this.props.message} type="text" value={this.state.message} onChange={e => this.handleChange(e)} /> <input onClick={e => this.handleSubmit(e)} type="submit"/> </form> <div class="content"> <ul> { this.state.messages.map((message) => ( <li>{ message }</li> )) } </ul> </div> </div> ); } } export default Form;
index.jsでgetState()
で取得したstateをpropsでFormコンポーネントまで渡し、placeholderに設定しただけ。
こんな風にpropsがちゃんと渡ってきていることがわかる。
formに文字を入力してstateの値を更新する
さっきまででreducerで初期値のstateを持ってきてviewに反映するというところはできた。今度は、formに値を入力してそれをviewに反映させたいと思う。
index.js
const render = () => { const state = store.getState(); console.log(state); ReactDOM.render( <App message={state.message} onMessageChange={message => store.dispatch({ type: 'CHANGE_MESSAGE', message})} />, document.querySelector('.container') ); }
onMessageChange={message => store.dispatch({ type: 'CHANGE_MESSAGE', message})}
という一文を追加。onMessageChange()
は関数でそれをAppコンポーネントに渡している。中身のstore.dispatch
はreducerを通してstoreを更新するための関数。CHANGE_MESSAGEというアクションと入力された文字であるmessageを渡す。
こうすることで、子コンポーネントでは、onMessageChange
関数を実行すると、store.dispatch関数によってstoreを更新することができるようになる。onMessage()
の中身が{message => store.dispatch({ type: 'CHANGE_MESSAGE', message})}
だからね。さっきと同じようにApp→Formとこの関数を渡していく。
import React, { Component } from 'react'; import Form from './Form'; class App extends Component { render() { return ( <Form message={this.props.message} onMessageChange = {this.props.onMessageChange} /> ); } } export default App;
Form.jsx
import React, { Component } from 'react'; class Form extends Component { constructor(props) { super(props); this.state = { message: "", messages: [], } } handleChange(e) { e.preventDefault(); this.props.onMessageChange(e.target.value); } handleSubmit(e) { e.preventDefault(); const { messages } = this.state || []; messages.push(this.state.message); this.setState({ messages, message: '', }); } render() { return ( <div> <form> <input placeholder={this.props.message} type="text" value={this.props.message} onChange={e => this.handleChange(e)} /> <input onClick={e => this.handleSubmit(e)} type="submit"/> </form> <div class="content"> <ul> { this.state.messages.map((message) => ( <li>{ message }</li> )) } </ul> </div> </div> ); } } export default Form;
ReactのsetState()
ではなく、dispatchを持っているonMessageChange()
を通してstoreを更新します。また、inputのvalueの値をthis.props.message
に変更する。こうしないと入力欄に文字を入力する事ができないはず。
本当にreducerを通っているのかを確認したいから、reducerにconsole.logを入れて見てみる。
export default (state={message: 'こんにちは'}, action) => { console.log(action); return state; }
これでブラウザに文字を入力してコンソールを見る。
こんな風にactionが表示されているのがわかる。ただ、このままではstateが更新されないから、更新されるようにreducerに追記していこう。
export default (state={ message: 'こんにちは'}, action) => { console.log('action', action); switch (action.type) { case 'CHANGE_MESSAGE': return Object.assign({}, state, { message: action.message }); default: return state; } return state; }
これでブラウザのコンソールを見てみると、stateの値が更新されて表示されているのが見えるだろう。
入力した値を画面上に表示させる
今までReactで文字を入力して画面に反映させていたものをReduxを使って反映させたい。まずは、reducerを改造して入力した文字がstateに入るようにする。
index.js
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { createStore } from 'redux' import App from './App'; import reducer from './reducers/'; const store = createStore(reducer); const render = () => { const state = store.getState(); ReactDOM.render( <App messages={state} onMessageSubmit={message => store.dispatch({ type: 'ADD_MESSAGE', message})} />, document.querySelector('.container') ); } render(); store.subscribe(render);
reducers/index.js
const message = (state, action) => { switch (action.type) { case 'ADD_MESSAGE': return { message: action.message, } case 'CHANGE_MESSAGE': return Object.assign({}, state, { message: action.message }); default: return state; } return state; } const messages = (state = [], action) => { switch (action.type) { case 'ADD_MESSAGE': return [ ...state, message(undefined, action) ] default: return state } } export default messages
関数が2つあるけど、これは入力されたメッセージを配列として管理したいから。["メッセージ1", "メッセージ2", "メッセージ3", ・・・]
のようなイメージ。メッセージというのは入力した文字。
store.dispatch({ type: 'ADD_MESSAGE', message})
が呼ばれる→messages()
が呼ばれtypeに合致するcaseが内が実行される。今回はADD_MESSAGEというtypeを持っているので、呼ばれるのは以下。
case 'ADD_MESSAGE': return [ ...state, message(undefined, action) ] defa
この中では、message()
という関数が呼ばれています。message()の中を見てみると、ADD_MESSAGEの中ではaction.message
を返すようになっているね。ここに入力したmessageが入っているのでこれを呼び出し元に返して、最終的に返されるのは、messages()
のreturn文。
case 'ADD_MESSAGE': return [ ...state, message(undefined, action) ]
messages()
から返ってきたmessage内容を内包した配列を返します。これで、配列の中に入力した内容が入ったものが新たなstateとして返ってくる。
dispatchを持った関数を渡していく
さっき作ったonMessageSubmit
という関数をApp→Formと渡していきます。
App.jsx
import React, { Component } from 'react'; import Form from './Form'; class App extends Component { render() { return ( <Form messages={this.props.messages} onMessageSubmit = {this.props.onMessageSubmit} /> ); } } export default App;
Form.jsx
import React, { Component } from 'react'; class Form extends Component { constructor(props) { super(props); this.state = { message: "", messages: [], } } handleSubmit(e) { e.preventDefault(); this.props.onMessageSubmit(this.textInput.value) this.textInput.value = '' } render() { return ( <div> <form onSubmit={e => this.handleSubmit(e)}> <input type="text" ref={(input) => this.textInput = input} placeholder={this.props.message} /> <input type="submit"/> </form> <div class="content"> <ul> { this.props.messages.map((object) => ( <li>{ object.message }</li> )) } </ul> </div> </div> ); } } export default Form;
Form.jsxのhandleSubmitの中でさっき渡してきたonMessageSubmit
を使います。もちろん関数に渡すのは入力したものですね。
stateを画面に表示させる
messageを表示している部分は下記です。
<div class="content"> <ul> { this.props.messages.map((object) => ( <li>{ object.message }</li> )) } </ul> </div>
この渡ってきているmessagesにはstore.getState()
の値が入っています。それらをmapで回して全てのメッセージを出力します。
実行するとこんな感じですね。
それぞれのコンポーネントでdispatchできるようにする
現状のindex.jsのコードを見てみるとこんな感じです。
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { createStore } from 'redux' import App from './App'; import reducer from './reducers/'; const store = createStore(reducer); const render = () => { const state = store.getState(); ReactDOM.render( <App messages={state} onMessageSubmit={message => store.dispatch({ type: 'ADD_MESSAGE', message})} />, document.querySelector('.container') ); } render(); store.subscribe(render);
store.dispatch()
を実行する関数onMessageSubmit()
を子コンポーネントに受け渡して、その関数を子コンポーネントでは使い、storeを更新する。だけど、関数をいちいちpropsとしてコンポーネントに受け渡して使うのは面倒。
それぞれのコンポーネントでstore.dispatch()
を使えるようにすれば、こんな風に関数を受け渡してそれを実行して・・・なんてことをしなくてもよくなるよね。だから、store自体を子コンポーネントに受け渡して、store.dispatch()
をそれぞれのコンポーネントで行う事ができるようにしていく。
index.js
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { createStore } from 'redux' import App from './App'; import reducer from './reducers/'; const store = createStore(reducer); ReactDOM.render( <App store={store} />, document.querySelector('.container') );
今までは、stateをここで取得してpropsで渡していたけど、storeを渡す事でそこからstateを取得する事ができる(store.getState())ので、渡すのはstoreだけにする。そして、storeに変更があったら再度描画するように関数化してstore.subscribe(render)
としていたけど、storeに変更があったらそれに関係する子のコンポーネントだけを再描画するようにする。
そのため、エントリーポイントのindex.jsではsubscribeせずに、子のコンポーネントをsubscribeする。子のコンポーネントをsubscribeすることによってそのコンポーネントでstoreの変更を検知して再描画するのかどうかを判断する事ができるようになる。
App.jsx
import React, { Component } from 'react'; import Form from './Form'; class App extends Component { render() { return ( <Form store={this.props.store} /> ); } } export default App;
ここはFormコンポーネントにstoreを渡すだけ。
Form.jsx
import React, { Component } from 'react'; class Form extends Component { constructor(props) { super(props); this.state = { message: "", messages: [], } } componentDidMount() { this.unsubscribe = this.props.store.subscribe(() => { this.forceUpdate(); }); } componentWillUnmount() { this.unsubscribe(); } handleSubmit(e) { e.preventDefault(); this.props.store.dispatch({ type: 'ADD_MESSAGE', message: this.textInput.value}) this.textInput.value = '' } render() { const state = this.props.store.getState(); return ( <div> <form onSubmit={e => this.handleSubmit(e)}> <input type="text" ref={(input) => this.textInput = input} placeholder={this.props.message} /> <input type="submit"/> </form> <div class="content"> <ul> { state.map((object) => ( <li>{ object.message }</li> )) } </ul> </div> </div> ); } } export default Form;
ここで変えたのは、まずstore.getState()
する事。
const state = this.props.store.getState();
ここで取得したstateを描画するようにします。
<div class="content"> <ul> { state.map((object) => ( <li>{ object.message }</li> )) } </ul> </div>
送信ボタンを押した時の処理も変更します。
handleSubmit(e) { e.preventDefault(); this.props.store.dispatch({ type: 'ADD_MESSAGE', message: this.textInput.value}) this.textInput.value = '' }
ここで、store.dispatch()
です。actionと入力されたメッセージをreducerに渡す。
subscribeする
storeが更新されたら、このコンポーネントを再描画させるためには、subscribe()を使ってこのコンポーネントとstoreを紐付けなければなりません。
componentDidMount() { this.unsubscribe = this.props.store.subscribe(() => { this.forceUpdate(); }); } componentWillUnmount() { this.unsubscribe(); }
storeのsubscribe関数を使ってこのコンポーネントとstoreを紐付けます。storeの変更を検知したら関数が動くけど、それで勝手に新しいstateが反映されたviewが描画されるわけではない。再描画させるために、forceUpdate()
を使います。これは、viewをレンダリングしろっていう関数。
forceUpdate | React 0.13 日本語リファレンス | js STUDIO
Providerを使う
storeをpropsとして渡していき、それぞれのコンポーネントでsubscribeしてstoreの変更を検知して描画するということをやってきたけど、これをそれぞれのコンポーネントに書いていくのは面倒。
reactでcontextという子孫コンポーネントでグローバルのように使えるものがあるけど、それと同じようなことをしていきたい。react-reduxにはProvider
というそれらを簡潔に記述する事ができるものがあるから、それを使っていこう。
index.js
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { createStore } from 'redux' import { Provider } from 'react-redux'; import App from './App'; import reducer from './reducers/'; ReactDOM.render( <Provider store={createStore(reducer)}> <App /> </Provider>, document.querySelector('.container') );
Providerでコンポーネントを包み、そこにstoreを渡す。こうすることで、子孫コンポーネントでstoreを引っ張って使う事ができる。storeがあるならそこからstore.dispatch()
も使えるというわけ。それぞれのコンポーネントでstore.dispatch()
を使いstoreを更新する事ができるようになるという事です。
今のままではcontext特有の記述をして、storeを取り出さないといけません。だけど、react-reduxにはconnect
という便利なものがあります。これは、stateをstoreから取ってきて、そのコンポーネント内でpropsとして扱えるようにしてくれ、dispatchを取ってきてコンポーネント内で扱えるようにしてくれるもの。
Form.jsx
import React, { Component } from 'react'; import { connect } from 'react-redux'; const mapStateToProps = state => ({ messages: state, }); const mapDispatchToProps = dispatch => ({ onMessageSubmit: message => dispatch({ type: 'ADD_MESSAGE', message}), }); class Form extends Component { constructor(props) { super(props); this.state = { message: "", messages: [], } } handleSubmit(e) { e.preventDefault(); this.props.onMessageSubmit(this.textInput.value); this.textInput.value = '' } render() { return ( <div> <form onSubmit={e => this.handleSubmit(e)}> <input type="text" ref={(input) => this.textInput = input} placeholder={this.props.message} /> <input type="submit"/> </form> <div class="content"> <ul> { this.props.messages.map((object) => ( <li>{ object.message }</li> )) } </ul> </div> </div> ); } } const ConnectedForm = connect(mapStateToProps,mapDispatchToProps)(Form); export default ConnectedForm;
connect関数を使う事で、コンポーネントとstateやdispatchを紐付けてくれる。これによって、以前書いていたsubscribeを記述することも不要になるから消しておこう。mapToStateToPropsとmapStateToDispatchというものが出てきた。
mapStateToProps
mapStateToPropsはその名の通り、コンポーネント内でstateをpropsとして扱えるようにするもの。
const mapStateToProps = state => ({ messages: state, });
ここでは、stateを取ってきてmessagesに当てている。このmessagesは入力した値を表示するために以下の部分で使っているよ。
<ul> { this.props.messages.map((object) => ( <li>{ object.message }</li> )) } </ul>
mapStateToDispatch
これはdispatchを受け取ってそれをpropsとして扱う事ができるようにするもの。
const mapDispatchToProps = dispatch => ({ onMessageSubmit: message => dispatch({ type: 'ADD_MESSAGE', message}), });
ここでは、dispatchを受け取って、それをonMessageSubmitという関数に当てている。このonMessageSubmit
という関数を使う事で、dispatchする事ができるようになるというわけだね。
ここまで書くと普通にReactを書いたように動くはず。
combineReducersを使ってstateにキーをつける
Form.jsxを見てみるとstateの取得は以下のように書いている。
const mapStateToProps = state => ({ messages: state, });
<ul> { this.props.messages.map((object) => ( <li>{ object.message }</li> )) } </ul>
その取得したmessagesの描画はこんな感じ。
だだ、これだとstateを直で持ってきてしまっていて一体何を持ってきているのか分かりにくい。state.messagesのようにキーを指定して取得してきたい。これを実現するのにcombineReducers()
というものを使っていきたい。
reducersのindex.jsを編集しよう。
reducers/index.js
import { combineReducers } from 'redux'; const message = (state, action) => { switch (action.type) { case 'ADD_MESSAGE': return { message: action.message, } case 'CHANGE_MESSAGE': return Object.assign({}, state, { message: action.message }); default: return state; } return state; } const messages = (state = [], action) => { switch (action.type) { case 'ADD_MESSAGE': return [ ...state, message(undefined, action) ] default: return state } } export default combineReducers({ messages });
reduxからcombineReducersを引っ張ってきてその引数にmessagesを入れている。ちょっと分かりにくいけど、Reactの省略記法を使っているから、愚直にやるとこんな感じ。
export default combineReducers({ messages : messages });
messagesというkeyにmessagesを当てている。こうすることによってstoreから引っ張ってくるときにはstore.messages
でこいつを取ってくる事ができる。
Form.jsx
const mapStateToProps = state => ({ messages: state.messages, });
state.messagesに変更。
<div class="content"> <ul> { this.props.messages.map((message) => ( <li>{ Object.values(message) }</li> )) } </ul> </div>
さっきと同じように書いてもいいけど、ちょっと分かりにくので修正。
最終的なコード
index.js
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { createStore } from 'redux' import { Provider } from 'react-redux'; import App from './App'; import reducer from './reducers/'; ReactDOM.render( <Provider store={createStore(reducer)}> <App /> </Provider>, document.querySelector('.container') );
App.jsx
import React, { Component } from 'react'; import Form from './Form'; class App extends Component { render() { return ( <Form /> ); } } export default App;
Form.jsx
import React, { Component } from 'react'; import { connect } from 'react-redux'; class Form extends Component { handleSubmit(e) { e.preventDefault(); this.props.onMessageSubmit(this.textInput.value); this.textInput.value = '' } render() { return ( <div> <form onSubmit={e => this.handleSubmit(e)}> <input type="text" ref={(input) => this.textInput = input} placeholder={this.props.message} /> <input type="submit"/> </form> <div class="content"> <ul> { this.props.messages.map((message) => ( <li>{ Object.values(message) }</li> )) } </ul> </div> </div> ); } } const mapStateToProps = state => ({ messages: state.messages, }); const mapDispatchToProps = dispatch => ({ onMessageSubmit: message => dispatch({ type: 'ADD_MESSAGE', message}), }); const ConnectedForm = connect(mapStateToProps,mapDispatchToProps)(Form); export default ConnectedForm;
reducers/index.jsx
import { combineReducers } from 'redux'; const message = (state, action) => { switch (action.type) { case 'ADD_MESSAGE': return { message: action.message, } case 'CHANGE_MESSAGE': return Object.assign({}, state, { message: action.message }); default: return state; } return state; } const messages = (state = [], action) => { switch (action.type) { case 'ADD_MESSAGE': return [ ...state, message(undefined, action) ] default: return state } } export default combineReducers({ messages });
まとめ
やっぱりReduxむずい。大規模のアプリケーションや仕様が増えていくもの、まだわかっていない部分があるものにはReduxを使う事が多いような気がするけど、理解して使いこなすのはなかなかきつそうだなあ。
最後まで読んでくれてありがとう。