Redux.combineReducers についてのメモ
コンポーネント毎に機能を分割する上で、 combineReducers
はとても役に立った。
しかしながらこれが持つ微妙な癖に少し戸惑ったのでメモを残しておきたい。
※ここにあるのは経験則です。ドキュメント読めばもっとスマートなやり方があるかもしれません。
グローバルステート
どのコンポーネントからも参照できるプロジェクト全体のステート。
combineReducers
が次のように設定されているとき、
export default combineReducers({ "reducerA": componentReducerA, "reducerB": componentReducerB });
グローバルステートは次のような形になる。
{ "reducerA": { ... }, "reducerB": { ... } }
各 Reducer のスコープ
combineReducers
によって結合される各 Reducer は、グローバルステート内に割り当てられたそれぞれのネームスペースのみ操作できる。
上記の例で言えば、 componentReducerA
は state.reducerA
の内部しか操作することはできない。
言い換えると、 componentReducerA
は state.reducerB
を操作することができない。
別コンポーネントのステートを更新するには
ActionCreator によって作成された Action は、 combineReducers
によって結合されたすべての Reducer に通知されるため、これを利用する。
最後に
子コンポーネントは Action だけ飛ばしてルートコンポーネントの Reducer でステート管理すればいいのでは、と思い始めている。
GitHubにソースを公開した
React + Reduxの拙い一例としていつか誰かの役に立てば、と思う。
歌詞タイムの歌詞がコピーできないのでむりやりコピーできるようにブックマークレットを作った
題の通り。
本当は歌詞タイムのページ内でコピーできるようにしたかったけどできなかったので苦肉の策。
作成したもの
歌詞オープナー(歌詞タイムのページでつかってね)
期待する動き
新しいタブが開いて歌詞が表示される。
結果
成功
手順など
一応メモ。
(function() { // 新しいタブを開く。 // `newTab` には新しいタブの `window` オブジェクトが入る。 var newTab = window.open(); // 歌詞タイムのページの歌詞部分の要素をクローンして新しい要素を作る。 var lyricContainer = lyrics.cloneNode(true); // クローン要素を新しいタブの `body` に挿入する。 newTab.document.body.appendChild(lyricContainer); })();
初心者によるReduxのふわっとしたイメージ解説
Redux + React に挑戦している。 なんとなく感覚はつかめてきたが小さい謎が多い。 感覚をつかむまでの苦しみがすごいのでメモを残しておきたい。
実際の Redux のライフサイクル
イメージ解説:「りんごがひとつ売れたので在庫情報を更新したい」
ユーザーさん(View):「りんごがひとつ売れたので在庫情報を更新したい」
受付さん(Action Creator):「わかりました。少し待っててくださいね。φ(・_・”)」
受付さん(Action Creator):「お待たせしました。これを事務さんに持っていってください。」
依頼書(Action):「【業務】りんごが売れたため在庫更新【個数】1」
事務さん(Reducer):「りんごが売れたため在庫更新、個数は1、ですね。わかりました。」
事務さん(Reducer):「書類棚から在庫管理表を取ってきて、」
- 書類棚(Store)
- 在庫管理表(State)
事務さん(Reducer):「新しい在庫管理表と依頼書を照らし合わせて新しい在庫管理表を作る。」
事務さん(Reducer):「それでは在庫更新しておきましたのでもう大丈夫ですよ。」
ユーザーさん(View):「ありがとうございました。」
テスト用プロジェクト
ROOT_DIRECTORY/ + node_modules/ + build/ + source/ | + renderer/ | | + components/ | | + app/ | | + container.js | | + view.js | | + reducer.js | | + actions.js | | | + index.html | + gulpfile.babel.js + .babelrc + package.json
source/index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Redux Sample</title> </head> <body> <div id="app"></div> <script src="./renderer/bundle.js"></script> </body> </html>
source/renderer/components/app/container.js
connect
は Redux と React をつなげる。
mapStateToProps
は Redux で管理する state を React コンポーネントの props にひもづける。
mapDispatchToProps
は Action Creator への呼び出しを React コンポーネントの props にひもづける。
React コンポーネントでは state ではなく props を使っていくことになる。
自分でも書いててよく分からんけど何回か書いてたら慣れる。
import React from "react"; import { connect } from "react-redux"; import Component from "./view"; import Actions from "./actions"; export default connect( mapStateToProps, mapDispatchToProps )(Component); function mapStateToProps(state) { console.log("container.js: mapStateToProps\n" + "state: " + JSON.stringify(state, null, " ")); return state.app; } function mapDispatchToProps(dispatch) { console.log("container.js: mapDispatchToProps"); return { "sellHandler": () => { dispatch(Actions.sell()); }, "recieveHandler": () => { dispatch(Actions.recieve()); } }; }
source/renderer/components/app/view.js
React のコンポーネント。
import React from "react"; export default class Component extends React.Component { render() { console.log("view.js: render\n" + "state: " + JSON.stringify(this.state, null, " ") + "\n" + "props: " + JSON.stringify(this.props, null, " ")); return ( <div> <table> <thead> <tr> <th>品名</th> <th>単価</th> <th>在庫</th> </tr> </thead> <tbody> <tr> <td>{this.props.product.name}</td> <td>{this.props.product.price}</td> <td>{this.props.product.stock}</td> </tr> </tbody> </table> <button onClick={this.props.sellHandler}>売る</button> <button onClick={this.props.recieveHandler}>入荷する</button> </div> ); } }
source/renderer/components/app/reducer.js
Action による state の変更を管理する。state の初期値もここで設定する。事務さん。
const initialState = { "product": { "name": "りんご", "price": 120, "stock": 100 } }; export default function reducer(state = initialState, action) { console.log("reducer.js: reducer\n" + "state: " + JSON.stringify(state, null, " ") + "\n" + "action: " + JSON.stringify(action, null, " ")); switch(action.type) { case "PRODUCT_SOLD": return Object.assign({}, state, { "product": { "name": state.product.name, "price": state.product.price - action.amount, "stock": state.product.stock } }); case "PRODUCT_RECIEVED": return Object.assign({}, state, { "product": { "name": state.product.name, "price": state.product.price + action.amount, "stock": state.product.stock } }); default: return state; } }
source/renderer/components/app/actions.js
Action を返し、state の変更パターンを定義する。受付さん。
export default { "sell": () => { console.log("actions.js: sell"); return { "type": "PRODUCT_SOLD", "amount": 1 }; }, "recieve": () => { console.log("actions.js: recieve"); return { "type": "PRODUCT_RECIEVED", "amount": 1 } } }
ログつきテストページ
デバッガーのコンソールタブにログがでる。
ページ読み込み時のログ
初期描画時に謎に Reducer が 3回も動いている。謎でしかない。
reducer.js: reducer state: { "product": { "name": "りんご", "price": 120, "stock": 100 } } action: { "type": "@@redux/INIT" } reducer.js: reducer state: { "product": { "name": "りんご", "price": 120, "stock": 100 } } action: { "type": "@@redux/PROBE_UNKNOWN_ACTION_v.1.7.7.f.r" } reducer.js: reducer state: { "product": { "name": "りんご", "price": 120, "stock": 100 } } action: { "type": "@@redux/INIT" } container.js: mapStateToProps state: { "app": { "product": { "name": "りんご", "price": 120, "stock": 100 } } } container.js: mapDispatchToProps view.js: render state: null props: { "product": { "name": "りんご", "price": 120, "stock": 100 } }
「売る」ボタンを押したときのログ
Action Creator => Reducer の流れで state
の値が変化していること、 state
変化後に render
が呼ばれていることがわかる。
stock
を変えたつもりがprice
が変わっている。凡ミス。mapStateToProps
って毎回呼ばれるのね。props
にひもづけたらお役御免かと思っていた。
actions.js: sell reducer.js: reducer state: { "product": { "name": "りんご", "price": 120, "stock": 100 } } action: { "type": "PRODUCT_SOLD", "amount": 1 } container.js: mapStateToProps state: { "app": { "product": { "name": "りんご", "price": 119, "stock": 100 } } } view.js: render state: null props: { "product": { "name": "りんご", "price": 119, "stock": 100 } }
「入荷する」ボタンを押したときのログ
Action Creator => Reducer の流れで state
の値が変化していること、 state
変化後に render
が呼ばれていることがわかる。
actions.js: recieve reducer.js: reducer state: { "product": { "name": "りんご", "price": 120, "stock": 100 } } action: { "type": "PRODUCT_RECIEVED", "amount": 1 } container.js: mapStateToProps state: { "app": { "product": { "name": "りんご", "price": 121, "stock": 100 } } } view.js: render state: null props: { "product": { "name": "りんご", "price": 121, "stock": 100 } }
全体的な View の作成が終わった
全体的な View の作成が終わった。
作成したもの
source/renderer/components/ 以下の view ファイル、 style ファイル
期待する動き
現行ページのレイアウトを再現する。
結果
成功
手順など
各コンポーネントディレクトリの下には view.js と style.css が配置されている。
ROOT_DIRECTORY/ + node_modules/ + build/ + source/ | + renderer/ | | + components/ | | | + app/ | | | + header/ | | | + main/ | | | + footer/ | | | + advertisement/ | | | + ranking-actress/ | | | + search-entry/ | | | + entries/ | | | + view.js | | | + style.css | | | | | + renderer.js | | | + styles/ | | + reset.css | | | + index.html | + gulpfile.babel.js + .babelrc + package.json
source/renderer/components/app/view.js
ページ全体の骨組みを作る。
import React from "react"; import styles from "./style"; import HeaderComponent from "../header/view"; import MainComponent from "../main/view"; import FooterComponent from "../footer/view"; export default class Component extends React.Component { render() { return ( <div className={styles.container}> <HeaderComponent /> <MainComponent /> <FooterComponent /> </div> ); } }
source/renderer/components/header/view.js
ヘッダー部分のコンポーネント
import React from "react"; import styles from "./style"; export default class Component extends React.Component { render() { return ( <header className={styles.container}> <h1 className={styles.title}> <a href="/">Nukindex</a> </h1> <p className={styles.description}> 毎日のおかずを提供するサイトです </p> </header> ); } }
source/renderer/components/footer/view.js
フッター部分のコンポーネント
import React from "react"; import styles from "./style"; export default class Component extends React.Component { render() { return ( <footer className={styles.container}> <p className={styles.row}> Copyright © 2017 Nukindex. All rights reserved. </p> <p className={styles.row}> Contact: <a href="mailto:ura.katz.1988@gmail.com">ura.katz.1988@gmail.com</a> </p> </footer> ); } }
source/renderer/components/main/view.js
メインコンテンツを入れるコンポーネント
import React from "react"; import styles from "./style"; import RankingActressComponent from "../ranking-actress/view"; import AdvertisementComponent from "../advertisement/view"; import SearchEntryComponent from "../search-entry/view"; import EntriesComponent from "../entries/view"; export default class Component extends React.Component { render() { return ( <main className={styles.container}> <RankingActressComponent /> <AdvertisementComponent> <a href="http://track.bannerbridge.net/click.php?APID=138222&affID=88332&siteID=172583" target="_blank"> <img src="http://track.bannerbridge.net/adgserv.php?APID=138222&affID=88332&siteID=172583" /> </a> </AdvertisementComponent> <SearchEntryComponent /> <EntriesComponent /> <AdvertisementComponent> <a href="http://track.bannerbridge.net/clickprod.php?adID=1661429&affID=88332&siteID=172583" target="_blank"> <img src="http://track.bannerbridge.net/adgprod.php?adID=1661429&affID=88332&siteID=172583" /> </a> </AdvertisementComponent> </main> ); } }
source/renderer/components/advertisement/view.js
広告部分のコンポーネント
import React from "react"; import styles from "./style"; export default class Component extends React.Component { render() { return ( <div className={styles.container}> <h2 className={styles.title}> 広告 </h2> <div className={styles.ads}> {this.props.children} </div> </div> ); } }
source/renderer/components/ranking-actress/view.js
女優ランキング部分のコンポーネント
import React from "react"; import styles from "./style"; export default class Component extends React.Component { render() { const actressRankingData = getActressRankingData(); const actressRankingItems = actressRankingData.map(actress => { return ( <li className={styles.item}> <a href="#">{actress}</a> </li> ); }); return ( <div className={styles.container}> <h2 className={styles.title}> 私的女優ランキング </h2> <p className={styles.description}> 個人的な女優ランキングです。 </p> <ol className={styles.list}> {actressRankingItems} </ol> </div> ); } } function getActressRankingData() { return [ "跡美しゅり", "橋本ありな", "市川まさみ", "月本れいな" ]; }
source/renderer/components/search-entry/view.js
検索部分のコンポーネント
import React from "react"; import styles from "./style"; export default class Component extends React.Component { render() { return ( <div className={styles.container}> <h2 className={styles.title}> 動画検索 </h2> <p className={styles.row}> <input className={styles.field} placeholder="動画を検索" /> </p> </div> ); } }
source/renderer/components/entries/view.js
動画リスト部分のコンポーネント
import React from "react"; import styles from "./style"; export default class Component extends React.Component { render() { const entriesData = getEntriesData(); const entryItems = entriesData.map(entryData => { const tagItems = entryData.tags.map(tag => { return ( <li className={styles.item_tag}> {tag} </li> ); }); return ( <li className={styles.item_entry}> <h2 className={styles.title_entry}> {entryData.title} </h2> <ul className={styles.list_tags}> {tagItems} </ul> </li> ); }); return ( <div className={styles.container}> <ul className={styles.list_entries}> {entryItems} </ul> </div> ); } } function getEntriesData() { return [ { "videoId": 12345678, "imageId": 0, "title": "動画タイトル", "tags": ["タグ1", "タグ2"] } ]; }
いろいろ調べながら書いた。 React の基本的な描画機能は使えていると思う。
次はデータの外部化と Redux の導入を頑張りたい。