Reactの続きです。前回は「コンテクスト(Context)」を使ってコンポーネントに値を直接渡しました。今回はReactの拡張ツール「Redux」を使って値の状態管理を事始めてみます。それではいきましょう!
【目次】
※参考:前回記事
【React】コンテクスト(Context)でネストされたコンポーネントに値を渡す - クモのようにコツコツと
※参考:Reactを習得するためにやったことまとめ
qiita.com
Reduxとは何か
今回も掌田さんの「React.js & Next.js入門」を参考に進める。
- 作者:掌田津耶乃
- 発売日: 2019/08/30
- メディア: Kindle版
書籍ではコンポーネントの次には「Redux」に入る。ReduxはReactを拡張する「状態管理ツール」。 公式サイトの「Getting Started」
Redux is a predictable state container for JavaScript apps.
It helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test. On top of that, it provides a great developer experience, such as live code editing combined with a time traveling debugger.
Reduxは、JavaScriptアプリ用の予測可能な状態コンテナーです。 一貫して動作し、さまざまな環境(クライアント、サーバー、ネイティブ)で実行され、テストが容易なアプリケーションを作成するのに役立ちます。 その上、ライブコード編集とタイムトラベルデバッガーの組み合わせなど、優れた開発者エクスペリエンスを提供します。
※参考:Getting Started with Redux | Redux
うむ。よくわからないw
Vue.jsのときにやった「Vuex」はReduxのVue.js版的な位置づけ。
※参考:【Vue.js】Vuexの「状態管理」はいったい何の状態を管理しているのか調べた - クモのようにコツコツと
読み直して、はい、そうだったそうだった。Vuexの元はReduxだが、さらに根元にFluxがあった。
コンポーネント開発をすると、コンポーネントを超えて共通化したいデータの状態をどう管理するか、という課題があり、それらを解決するのがこれらの「状態管理ツール」なわけか。
Reduxの仕組み(ストア、プロバイダー、レデューサー)
掌田さんの書籍によるとReduxは下記のような仕組みがある。
- ストア:データ(ステート)を保管、管理する場所
- プロバイダー:ストアを他のコンポーネントに受け渡す仕組み
- レデューサー:ストアに保管されているステート を変更する仕組み
1つ目のストアはVuexでもあった。
※参考:【Vue.js】Vuexのストアに値を保管してコンポーネントに表示する - クモのようにコツコツと
Reactのステートについてはこちら。
※参考:【React】Reactプロジェクトでステートを事始め(setState()、タイマー処理、イベント処理) - クモのようにコツコツと
2つ目のプロバイダーは前回のコンテクスト(Context)でもあった。
※参考:【React】コンテクスト(Context)でネストされたコンポーネントに値を渡す - クモのようにコツコツと
3つ目のレデューサー(Reducers)って何ざんしょ。
A reducer (also called a reducing function) is a function that accepts an accumulation and a value and returns a new accumulation.
リデューサー(リデューシング関数とも呼ばれます)は、累積と値を受け入れ、新しい累積を返す関数です。
※参考:Reducers | Redux
「累積と値を受け入れ、新しい累積を返す」?むむむ。。
これが値を操作する、ということかな?
Reduxをインストールしてみる
まずはどんなものなのか体験したい。そのためには、兎にも角にもれっつ・いんすとぉる!
ターミナルのcdコマンドでフォルダに移動
cd /(パス)/react_app
Reduxをインストール
npm install --save redux
次にReactとReduxを融合するパッケージ「React Redux」をインストール
npm install --save react-redux
package.jsonを見ると両方入っている!
"dependencies": { "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.5.0", "@testing-library/user-event": "^7.2.1", "react": "^16.13.1", "react-dom": "^16.13.1", "react-redux": "^7.2.0", // 追加 "react-scripts": "3.4.1", "redux": "^4.0.5" // 追加 },
index.appの修正
以前、ステート事始めでやったような羊を数えるカウンターを作ってみるる。
※参考:【React】Reactプロジェクトでステートを事始め(setState()、タイマー処理、イベント処理) - クモのようにコツコツと
reduxとreact-reduxをインポート
まず冒頭でredux
とreact-redux
をインポートする
import { createStore, combideReducers } from 'redux'; import { Provider } from 'react-redux';
連想配列のキーを読み込む形。
ステートの初期値を設定
次にステート(データ)の初期値を設定する。
// ステート let counter = { counter: 0 }
- 変数
counter
に連想配列を設定 conter
キーの値は0
羊の数を数えるのでcounter
という名前にした。初期値は0
レデューサーを設定
次にレデューサーを作る。ステートの値を変更する仕組みね。
// レデューサー function HitsyjiCounter(state = counter, action) { switch (action.type) { case 'Plus': return { counter: state.counter + 1 }; case 'Minus': return { counter: state.counter - 1 }; default: return state; } }
- 関数
HitsyjiCounter
を作成(第1引数のstate
の値をcounter
に、第2引数はaction
) - switch文の条件に
action
のtype
を入れる type
がPlus
であれば連想配列を返す
counter
キーにstate
のcounter
足す1
を入れるtype
がMinus
であれば連想配列を返す
counter
キーにstate
のcounter
引く1
を入れる- それ以外は
state
の初期値を返す
第1引数のstate
に先程のステートcounter
を読み込む。
第2引数のaction
を用意すると、デフォルトでtype
プロパティが使える。これが分岐条件になる。(のちほどApp.jsで使う)
switch文はif文と違って並列な分岐条件。break
で処理を抜けるのが基本だが、return
も処理を抜けることができるのでbreak
は不要。
※参考:【JSの基本-後編】書ける前に読む!HTML、CSS、JSの書式-5 - クモのようにコツコツと
ストアを作成
次にストアを作成する。
// ストア作成 let store = createStore(HitsyjiCounter);
- 変数
store
でcreateStore()
を実行(引数はHitsyjiCounter
)
createStore()
はReduxの組み込みメソッドで名前の通りストアをを作成する。引数で先程のHitsyjiCounter
を読み込む。
プロバイダ
最後にをプロバイダー設定。ストアを他のコンポーネントに受け渡す仕組み。
// レンダリング ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') );
Provider
タグでApp
タグを囲うProvider
タグのstore
属性の値store
これでstore
の値がApp
コンポーネントに渡される。
App.jsの修正
次はApp.jsを修正する
react-reduxをインポート
Appコンポーネントでもreact-redux
をインポートする
import { connect } from 'react-redux';
ステートのマッピング
まずステート をマッピング(関連付け)する
// ステートのマッピング function mappingState(state) { return state; }
- 関数
mappingState()
を設定(引数はstate
) return
でstate
を返す
先程のindex.jsのストアから渡されたステートの値をこの関数でマッピングする。
Appコンポーネント
処理の大枠
// Appコンポーネント class App extends React.Component { constructor(props) { // 中略 } render() { // 中略 } }
- コンストラクタ設定
- JSXレンダリング
コンストラクタ設定
constructor(props) { super(props); }
Reactのクラスで出てきたコンストラクタconstructor()
。中のsuper()
キーワードは親クラスを参照する。
※参考:【React】クラスを使ったコンポーネントの書き方(React.Component、render()、super()) - クモのようにコツコツと
JSXレンダリング
次にJSXをレンダリング
render() { return ( <div className="App"> <header className="App-header"> <! -- 中略 --> <h1>羊は何匹?</h1> <Message /> <Button /> </header> </div> ); }
中でさらにMessage
コンポーネント、Button
コンポーネントを読み込んでいる。
ストアのコネクト
最後にストアのコネクト
// ストアのコネクト
App = connect()(App);
App
でconnect()
を作成し、引数App
にストアをコネクトする。
connect()
は「react-redux」の組み込みメソッドでコンポーネントとストアを接続する。
カッコが二つ続くのがあまり見かけない書き方!
これは関数の戻り値が関数のときに、戻り値の関数を作成すると同時に実行もする、という動きになる。
下記の記事の「かっこかっこ言ってる」を参照。
※参考:JavaScriptを読んでて「なにこれ!?」と思うけれど調べられない記法8選。 - Qiita
Messageコンポーネント
ステートを読み込む
次にAppコンポーネントに読み込まれているMessageコンポーネント部分
// メッセージのコンポーネント class Message extends React.Component { render() { return ( <div> <p>羊が{this.props.counter}匹</p> </div> ); } }
- JSXレンダリング部分で
props
のcounter
キーを読み込んで表示する
ストアのコネクト
// ストアのコネクト
Message = connect(mappingState)(Message);
Message
でconnect()
を作成(引数でmappingState
のストア設定を読み込む)し、引数Message
をコネクトする。
今度は一つ目のカッコに先程マッピングした関数mappingState
のステートが入る。
これによってthis.props.counter
などより細かいプロパティの値を得ることができる。
Buttonコンポーネントでディスパッチを設定
最後にButtonコーポーネントの設定
処理の大枠
// ボタンのコンポーネント class Button extends React.Component { constructor(props) { // 中略 } // ボタンクリック ButtonClickDispatch(e) { // 中略 } render() { // 中略 } }
- コンストラクタ設定
- ディスパッチ設定
- レンダリング設定
コンストラクタ設定
constructor(props) { super(props); this.ButtonClickDispatch = this.ButtonClickDispatch.bind(this); }
constructor()
メソッドでコンストラクタ作成(引数はprops
)super()
メソッド実行(引数props
)ButtonClickDispatch
でButtonClickDispatch
をバインドbind()
(引数はthis
)
bind()
はJSの組み込みメソッド
bind() メソッドは、呼び出された際に this キーワードに指定された値が設定される新しい関数を生成します。この値は新しい関数が呼び出されたとき、一連の引数の前に置かれます。
※参考:Function.prototype.bind() - JavaScript | MDN
これでButtonClickDispatch
という関数が生成された。
ディスパッチ設定
// ボタンクリック ButtonClickDispatch(e) { if (e.shiftKey) { this.props.dispatch({ type: 'Minus' }); } else { this.props.dispatch({ type: 'Plus' }); } }
ButtonClickDispatch()
関数を実行(引数はイベントのe
)- もしシフトキーを押したら
props
でdispatch()
を実行(type
をMinus
に) - さもなくば
props
でdispatch()
を実行(type
をPlus
に)
シフトを押しながらクリックするとカウントがマイナスになる。普通のクリックはプラス。
「ディスパッチ」とはマルチタスクのプログラムに機能を割り当てることらしい
ディスパッチとは、複数のプログラムを実行中のマルチタスクオペレーティングシステムにおいて、プログラムに実行権を渡すことである。
※参考:ディスパッチとは (dispatch): - IT用語辞典バイナリ
これで関数実行のタイミングで条件によってtype
を変えてレデューサーの値を操作する。
レンダリング設定
JSXのレンダリング
render() { return ( <div> <button onClick={this.ButtonClickDispatch}> 数える! </button> </div> ); }
- buttonタグで
onClick
属性を設定(値はButtonClickDispatch
関数)
これでボタンを押したタイミングでButtonClickDispatch
関数が実行される。
ストアのコネクト
最後にストアのコネクト
// ストアのコネクト
Button = connect()(Button);
Button
でconnect()
を作成し、引数Button
にストアをコネクトする。
ブラウザ動作
ターミナルでReactを起動する。
npm start
おお!ステートの値がちゃんと読み込まれて表示されている!
「数える」ボタンをクリックすると… 羊の数が増えた!
シフトを押しながらボタンを押すと… 羊の数が減った!
index.jsのストアのステートと、それを操作するレデューサー設定がプロバイダーによってApp.jsに渡されて、さらにApp.js上の操作によって値のと変更が実現された!
JSコード
index.js全体
import React from 'react'; import ReactDOM from 'react-dom'; import { createStore, combideReducers } from 'redux'; import { Provider } from 'react-redux'; import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; // ステート let counter = { counter: 0 } // レデューサー function HitsyjiCounter(state = counter, action) { switch (action.type) { case 'Plus': return { counter: state.counter + 1 }; case 'Minus': return { counter: state.counter - 1 }; default: return state; } } // ストア作成 let store = createStore(HitsyjiCounter); // レンダリング ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') ); serviceWorker.unregister();
App.js全体
import React, { Component } from 'react'; import { connect } from 'react-redux'; import Img from './Img'; import './App.css'; // ステートのマッピング function mappingState(state) { return state; } // Appコンポーネント class App extends React.Component { constructor(props) { super(props); } render() { return ( <div className="App"> <header className="App-header"> <div className="App-images"> <Img align="left" /> <Img align="center" /> <Img align="rignt" /> </div> <h1>羊は何匹?</h1> <Message /> <Button /> </header> </div> ); } } // ストアのコネクト App = connect()(App); // メッセージのコンポーネント class Message extends React.Component { render() { return ( <div> <p>羊が{this.props.counter}匹</p> </div> ); } } // ストアのコネクト Message = connect(mappingState)(Message); // ボタンのコンポーネント class Button extends React.Component { constructor(props) { super(props); this.ButtonClickDispatch = this.ButtonClickDispatch.bind(this); } // ボタンクリック ButtonClickDispatch(e) { if (e.shiftKey) { this.props.dispatch({ type: 'Minus' }); } else { this.props.dispatch({ type: 'Plus' }); } } render() { return ( <div> <button onClick={this.ButtonClickDispatch}> 数える! </button> </div> ); } } // ストアのコネクト Button = connect()(Button); export default App;
ソース(GItHub)
※参考:GitHub - ryo-i/create-react-app-test: create-react-app事始め
最後に
Reduxによる値の受け渡しと変更を実現できました。新しいメソッドや書き方が出てきてまだ慣れないが、Reduxを通して値を受け渡すことでコンポーネント間の値のバケツリレーが起これない仕組みになっていると感じました。
次回も引き続きReduxでもう少し凝った仕組みを作ってみたく思います。それではまた!
続き書きました!Reduxはいったん中断してReactのCSS設定について書きました。
※参考:ReactのCSS設定方法について調べたこと(className属性、style属性、CSS Modules、CSS in JS、UIフレームワーク) - クモのようにコツコツと
※参考:Reactを習得するためにやったことまとめ
qiita.com