Help us understand the problem. What is going on with this article?

ノイズを剥がしてreact with reduxのカタチを見つめ、段階的に理解を深める 〜 JSおくのほそ道 #039

More than 1 year has passed since last update.

こんにちは、ほそ道です。

ここ最近、大人数でReactNativeを扱う機会があり、そのときに皆が苦戦していた傾向として
「reactとreduxの組合せ、もっと言えばreact-reduxというモジュールのボイラープレート(おまじない的な部分)が良くわからないのではないか?」
と思って色々サンプルを作ってコミュニケーションしてみた所、我が意を得たり!という反応が得られたので、その時に話したことやネタを展開してみようと思います。

シンプルなものから段階的に見ていくほうが理解を深めていきやすいのでは、と思い2段階ステップを踏んでいきます。
結論、Step1よりもStep2の方がベターケースであると考えています。

また、文中で幾つかの考え方を「ノイズ」と表現していますが、あくまで最小限のカタチを考える上で「無くても良い」という意味でノイズと言っているだけなので、「ない方がいい」「使ってはならぬ」とは特に思っておりませんので、前提としてご了承いただければと思います。
それぞれ、ひとつのアプローチのカタチとして捉えていただければ幸いでございます。

作るもの

同期的アクション、非同期的アクションそれぞれを作ります。
やっている事はクリックイベントで状態のboolean値をトグルするだけです。

サンプル(ブランチでstep1/2を分けていて、js版とtypescript版を作っています)

Step1: react-reduxを使わないカタチ

まずは、ひとつの呪縛ではないかと感じられた点。
react-redux使わないといけないのではないか?
→使わずとも動くものは作れます。
で、Reduxが単体で提供しているカタチを意識して、それを踏襲することをStep1とします。

reduxのベーシックなカタチに当てはめる

下記の駄図1を中心に見ていきたいと思います。図のviewは特にreactであることを特定していません。
subscribe(f)とpublishの間にうっすい赤線が引いてありますが、状態変更時に登録済みコールバックがコールされることをなにか示してみただけなのであまり気にしないでください(笑)。

駄図1

  • 0: viewからstore.subscribeをコールし、変更通知を受け取れるようにしておく
  • 1: 何かイベントが発生し、状態の更新を行う必要が発生したら、store.dispatchをコールし、storeに変更内容(action)を伝える
  • 2: reducerの中でactionをハンドリングして状態が更新される
  • 3: 状態が更新されるとsubscribeの引数として登録していたコールバックが呼び出される
  • 4: store.getStateをコールし更新された状態を受理する

上記の流れをview=reduxとして実装してみます。
reactでこのパターンを適用した場合、状態はviewとreducer両方に保持される事になります。
理解の為の第一段階として、まずはいきますので、react with reduxに最適化を進めるのは次のStep2で行っていきます。

実装してみる

流れをぶった切るようですが、まずはactionsから見ていきます。
viewとreducerをつなぐactionという存在の解釈は、前提として認識しておくと話が進めやすいと思ったからです。

actions
export function syncAction(x) {
  const nextX = !x;
  return {
    type: 'SYNC_ACTION',
    x: nextX
  }
}

function async(y) {
  return new Promise((resolve) => {
    resolve(!y);
  })
}

export async function asyncAction(y) {
  const nextY = await async(y);
  return {
    type: 'ASYNC_ACTION',
    y: nextY
  };
}

actionsは更新計算を行い、オブジェクトに包んで返します。
最小限のカタチを分析するに辺り、「ActionCreators」という概念は排除しています。
ActionCreatorsという概念の登場は必須であるというノイズは剥がしておきたいなと思いました。
必要なのはアクションというオブジェクトをreducerに渡すことである、という考えに基づけば、
スタート地点として、Actionオブジェクトを返すactionという関数があれば十分であるとを考えています。

次に、viewを見ていきます。

view
import * as React from 'react'
import * as ReactDOM from 'react-dom';
import {store} from './Store'
import {asyncAction, syncAction} from './Actions'
import {defaultState} from "./Reducer";

class MyView extends React.Component {

  constructor() {
    super();
    this.state = defaultState;
  }

  componentDidMount() {
    store.subscribe(() => {
      this.setState(store.getState());
    })
  }

  render() {
    return (
      <div>
        <div onClick={() => store.dispatch(syncAction(this.state.x))}>
          {`sync value ${String(this.state.x)}`}
        </div>
        <div onClick={async () => store.dispatch(await asyncAction(this.state.y))}>
          {`async value ${String(this.state.y)}`}
        </div>
      </div>
    );
  }
}

ReactDOM.render(<MyView/>, document.getElementById('contents'));

駄図1のフローに即したカタチになっています。
componentDidMountstore.subscribeをコールして変更通知の予約。
イベントが発生したらstore.dispatchをコールして変更をstoreに依頼。
変更通知を受け取ったらstore.getStateをコールして自身が持つ状態を更新。
駄図1と同様のカタチになっております。

非同期なactionの呼び出し方としては、asyncな関数をawaitでaction値を取得してからdispatchに渡しています。
ここにも非同期なactionの生成にはmiddlewareを使わねばならないというノイズがあったように思いますので、
このようなカタチでもシンプルに表現可能であるというのが言いたかったことです。

最後にreducerも見ていきます。

reducer
export const defaultState = {x: true, y: true};

export const AppReducer = ((state = defaultState, action) => {
  switch (action.type) {
    case 'SYNC_ACTION': {
      return Object.assign({}, state, {x: action.x});
    }
    case 'ASYNC_ACTION': {
      return Object.assign({}, state, {y: action.y});
    }
    default: {
      return defaultState;
    }
  }
});

reducerはactionに包まれた値をもって、状態の更新だけを行っています。

Step1は以上です。
細部を見たい、実際に動かしたい場合はサンプルのブランチstep1_jsまたはstep1_tsを御覧くださいませ。

Step1考察

まず、react-reduxモジュールだけを体験し、苦しんだ人がこのステップを踏んだ場合、
だいぶ全体の構成・フローがシンプルに感じられ、理解が出来るのではないでしょうか?

そして、Step1から2に進むに辺り、改善したい課題は下記の2点です。
・状態がreducerとview両方に保持されている
・viewのライフサイクルメソッドハンドリングがあるためにviewの見通しが悪くなる。

ほそ道は、このあたりを改善するためにreact-reduxというモジュールは生まれたのだと思っています。文献などを探ったわけではないですが。
さらにsetState問題をクリアするに辺り、下記のような内容も参考になると思います。
Container Component
Functional Component


Stap2: react-reduxに乗っかったカタチ

さて、react-reduxを登場させます。

フローを考える

今度は下記の駄図2を中心に見ていきたいと思います。
駄図2は駄図1をベースに、新規の概念ややり取りを青字で示しています。

駄図2

  • 前提としてviewはprops引数を適用すれば、ReactComponentを返す関数となっており、内部状態は持たないようにStep1から変わっています。
  • 0: Providerの子要素としてconnectで生成されたコンポーネントを指定する
  • 1: 何かイベントが発生し、状態の更新を行う必要が発生したら、dispatchをコールし、storeに変更内容(action)を伝える ★変更なし
  • 2: reducerの中でactionをハンドリングして状態が更新される ★変更なし
  • 3: 状態が更新されるとconnectに部分適用で登録していた変換コールバック関数がpropsを生成し、関数として定義されたviewの引数に入ってくる

実装してみる

まず、actionsとreducerはStep1から変更ありません
駄図2上も1,2のステップは変更ありませんし、ここに変化がないのは自然であることのように思います。

では、改善できたかの結論としてのviewを見ていきます。

view
import * as React from 'react'
import {asyncAction, syncAction} from './Actions'

export const MyView = ({state, dispatch}) => {
  return (
    <div>
      <div onClick={() => dispatch(syncAction(state.x))}>
        {`sync value ${String(state.x)}`}
      </div>
      <div onClick={async () => dispatch(await asyncAction(state.y))}>
        {`async value ${String(state.y)}`}
      </div>
    </div>
  );
};

subscribegetStateのコールが無くなりました。
setStateで内部状態を更新しなければならないような構成から開放されました。
Step1の課題は解決されたように思います。
それでは、このカラクリを知るためにreact-reduxの道具たちの使いっぷりを見ていきましょう。

App
import * as React from 'react'
import * as Redux from 'redux'
import * as ReactDOM from 'react-dom';
import {connect, Provider} from 'react-redux'
import {AppReducer} from "./Reducer";
import {MyView} from "./MyView";

export const appStore = Redux.createStore(AppReducer);

const view = (props) =>
  <MyView state={props.state} dispatch={props.dispatch}/>;

const App = connect(s => ({state: s}))(view);

ReactDOM.render(
  <Provider store={appStore}>
    <App/>
  </Provider>,
  document.getElementById('contents')
);

connectの中ではボイラープレート上、mapDispatchToPropsと呼ばれる関数の設置を省略しました。
このようにすることでstoreのdispatch関数がpropsに流れてくるようです。
ここは、bindActionCreators×actionCreatorsを使わなければならないというノイズも剥がすことが出来ればなと思っているポイントです。
actionCreatorsを設置すると、いわゆるreduxフロー図のComponent->Action->Reducerという流れが成立しているように見えやすい、
というメリットはあるのではないかと思うのですが、この概念を省略することでactionは単なるActionを生成する関数というシンプルさを生むことにもなるのかなと。
この辺、最終的にどのような構成を選び取るかは好き好きで良いのではないかと思います。

あらためてサンプル(ブランチでstep1/2を分けていて、js版とtypescript版を作っています)を貼っておきます。TypeScript版は型の解決がなんだかややこしかったですが、コンパイラの静的チェックは通る状態になっております。

今回は以上です。
react-reduxの理解に苦しんでいた方にとって、少しでも助けになるものであれば幸いです。
また、独自解釈をしている部分に関して、「それはこうだよ」というフィードバックをいただければありがたいです。

連載目次はこちら

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away