読者です 読者をやめる 読者になる 読者になる

Nukindex 開発ブログ

アダルトサイトNukindexの開発ブログです

Redux.combineReducers についてのメモ

コンポーネント毎に機能を分割する上で、 combineReducers はとても役に立った。

しかしながらこれが持つ微妙な癖に少し戸惑ったのでメモを残しておきたい。

※ここにあるのは経験則です。ドキュメント読めばもっとスマートなやり方があるかもしれません。

グローバルステート

どのコンポーネントからも参照できるプロジェクト全体のステート。

combineReducers が次のように設定されているとき、

export default combineReducers({
  "reducerA": componentReducerA,
  "reducerB": componentReducerB
});

グローバルステートは次のような形になる。

{
  "reducerA": {
    ...
  },
  "reducerB": {
    ...
  }
}

各 Reducer のスコープ

combineReducers によって結合される各 Reducer は、グローバルステート内に割り当てられたそれぞれのネームスペースのみ操作できる。

上記の例で言えば、 componentReducerAstate.reducerA の内部しか操作することはできない。

言い換えると、 componentReducerAstate.reducerB を操作することができない。

コンポーネントのステートを更新するには

ActionCreator によって作成された Action は、 combineReducers によって結合されたすべての Reducer に通知されるため、これを利用する。

f:id:katz_ura:20170504233918j:plain

最後に

コンポーネントは Action だけ飛ばしてルートコンポーネントの Reducer でステート管理すればいいのでは、と思い始めている。

GitHubにソースを公開した

React + Reduxの拙い一例としていつか誰かの役に立てば、と思う。

github.com

歌詞タイムの歌詞がコピーできないのでむりやりコピーできるようにブックマークレットを作った

題の通り。

本当は歌詞タイムのページ内でコピーできるようにしたかったけどできなかったので苦肉の策。

yossense.com

作成したもの

歌詞オープナー(歌詞タイムのページでつかってね)

期待する動き

新しいタブが開いて歌詞が表示される。

結果

成功

手順など

一応メモ。

(function() {
  // 新しいタブを開く。
  // `newTab` には新しいタブの `window` オブジェクトが入る。
  var newTab = window.open();

  // 歌詞タイムのページの歌詞部分の要素をクローンして新しい要素を作る。
  var lyricContainer = lyrics.cloneNode(true);

  // クローン要素を新しいタブの `body` に挿入する。
  newTab.document.body.appendChild(lyricContainer);
})();

初心者によるReduxのふわっとしたイメージ解説

Redux + React に挑戦している。 なんとなく感覚はつかめてきたが小さい謎が多い。 感覚をつかむまでの苦しみがすごいのでメモを残しておきたい。

実際の Redux のライフサイクル

f:id:katz_ura:20170427000805p:plain

イメージ解説:「りんごがひとつ売れたので在庫情報を更新したい」

f:id:katz_ura:20170426234316p:plain

ユーザーさん(View):「りんごがひとつ売れたので在庫情報を更新したい」

f:id:katz_ura:20170426234601p:plain

受付さん(Action Creator):「わかりました。少し待っててくださいね。φ(・_・”)」

受付さん(Action Creator):「お待たせしました。これを事務さんに持っていってください。」

f:id:katz_ura:20170426234640p:plain

依頼書(Action):「【業務】りんごが売れたため在庫更新【個数】1」

f:id:katz_ura:20170426235023p:plain

事務さん(Reducer):「りんごが売れたため在庫更新、個数は1、ですね。わかりました。」

f:id:katz_ura:20170426235213p:plain

事務さん(Reducer):「書類棚から在庫管理表を取ってきて、」

f:id:katz_ura:20170426235504p:plain

  • 書類棚(Store)
  • 在庫管理表(State)

f:id:katz_ura:20170427000420p:plain

事務さん(Reducer):「新しい在庫管理表と依頼書を照らし合わせて新しい在庫管理表を作る。」

f:id:katz_ura:20170427000631p:plain

事務さん(Reducer):「それでは在庫更新しておきましたのでもう大丈夫ですよ。」

f:id:katz_ura:20170426234316p:plain

ユーザーさん(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
  }
}

nukindex.com

全体的な 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 の導入を頑張りたい。

nukindex.com