React-Reduxの使い方をやっと覚えたので動かした

javascript

前回(React-Reduxと友達になりたい!)で1つのファイルにまとめてみるとは言ったものの
逆にわけがわからなくなったので結局機能別にファイル分けすることにしました。

今日、Reactに使う時間をかなりオーバーしてしまったのですが
何とかReact-Reduxと友達になれたので、後悔はないです。

create-react-appで生成してそこにReduxとreact-reduxを組み込んで
ボタンを押して数値を増やすだけのアプリケーションがやっと動いてくれたので
忘れないうちに記事書きだしてしまおうと思います。

React-Reduxの解説記事、javascriptのせいかオレオレ記法な人が多くてかなり苦労しました。
なんでこの人はclassを使うけどこの人はclassを使わなかったりしてるんだ!と。

es6の書き方だったりes5の書き方だったり統一性ないのが、調べてもわかり辛い原因なんじゃないかなと。
柔軟な書き方が出来る分、統一性が無くなるのが問題点ですね……

そのせいでどんどんjsが苦手になって行く気がします。
なので早くTypeScriptを身に付けて、統一性のある書き方にしたいところですね。

それでは理解したところまでですが、動いたので書き記しておきます。

React-Reduxで必要な要素

前回の記事「React-Reduxと友達になりたい!」ではReact-Reduxの持っている「Provider」「connectメソッド
を紹介しました。それらを使って組んでいく前に必要な要素を洗い出してみましょう。

  • store
  • reducer
  • action / actionCreator
  • component
  • container
  • Provider

大きく分けてこの6つになります。
1ずつ簡単に説明していきましょう。

store

まずReduxを使う理由の一つとしてこれですね。
状態を一元管理します。reduxをインポートして使えるようにします。

import { createStore } from 'redux'
import reducer from './reducers/reducer'
store = createStore(reducer)

reducer

storeの持つ状態とactionが送ってきた手続きを組み合わせて新しい状態を返します。
別ファイルで作っておいて、storeを作るときのcreateStoreの引数に渡します。(上記コード参照)

reducerは後述のactionを元に処理を分岐して
storeの保持しているstateを使って、新たなstateを返すだけの純粋関数となっています。

// ステート初期化用
const initialState = {
  hogehoge:1
}

// リデューサーを定義 
export default function reducer(state = initialState, action) {
  // 引数のアクションタイプ別で処理を切り替える。該当しなければ何もせずそのままstateを返す
  switch(action.type) {
    case 'INC':
      return {hogehoge: state.hogehoge + 1}
    case 'DEC':
      return {hogehoge: state.hogehoge + 1}
    default:
      return state
  }
}

INCというアクションタイプがきたら
ステートのhogehogeにプラス1した新しいkey-valueオブジェクトを返します。

DECも書いていますが、今回は使いません。
復習としてactionを増やしてマイナス1出来るか試すのもいいと思います。

action / actionCreator

ここがわからない原因の1つでした actionCreatorという関数があるのかなって思ってたらどうやら
actionを作り出すための自作関数だということです。

ようはactionはただのkey-valueであって、typeというキーは必須であとは任意のキーを持たせることが出来ます。
actionCretatorはそれをラップして返すシンプルな関数というだけです。

export function plusHogeProcess() { // この関数がactionCreator
  return {
    type: 'INC' // これがaction
  }
}

component

純粋なReactコンポーネントです。
特に変更はないですが、こうやって省くと訳が分からなくなるので一応載せておきます。
create-react-appで生成したApp.jsで必要なものだけ取り出したものです。

import React,{ Component } from 'react'
import logo from '../logo.svg'

export default class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" width="200px" />
        </header>
        <button onClick={() => this.props.handleClick()}>INC: { this.props.hogehoge }</button>

      </div>
    );
  }
}

ロゴはあってもなくてもどちらでもいいです。
見た目がわかりやすいようにいれただけなので。

注目してほしいのがbuttonタグの部分ですね。
ここでpropsとして受け取ったdispatchとstateを使用しています。

 

これは次に紹介するcontainerでconnectメソッドを使うことによって
プロパティとしてこのコンポーネントに渡すことができるのです。

container

React-Reduxは見た目用のコンポーネント(上記のcomponent)と機能用のコンポーネントを分けるのがいいらしいです。
その機能用のコンポーネントがcontainerになります。

どっちも1つのファイルで記述しても問題ないですが、
見た目用コンポーネントは再利用するために分けることが多いそうです。

今回も分ける必要はないのですが今後のことを考えて分けてみました。

import { connect } from 'react-redux'
import { plusHogeProcess } from '../actions/action'
import App from '../components/App'

 const mapStateToProps = state => {
   const { hogehoge } = state
   return { hogehoge }
 }

 const mapDispatchToProps = dispatch => {
   return {
    handleClick: () => { dispatch(plusHogeProcess()) }
   }
 }

 export default connect(mapStateToProps,mapDispatchToProps)(App)

ここでやっていることがreact-reduxがもつconnectを使えるようにしているのと
作ったactionCreator読み込んでいるのと、componentのAppコンポーネントの読み込み。

そしてmapStateToPropsとmapDispatchToPropsの定義ですね。

これが曲者でした。やってることは至極単純なのですが
これに何を入れたらいいのか全く分からず1時間ぐらい消費しました。

この名前は自由に決められますが、一応公式名としてはmap~となっているので
公式に沿ってやっています。

mapStateToProps

名前の通りstateをpropsに変換するものです。
stateの中から必要なstateを取り出してReactコンポーネントに渡すことが出来ます。
今回はhogehogeが欲しいのでstateからhogehogeを取り出して返しているだけです。

mapDispatchToProps

こちらも名前の通りdispatch関数をpropsとして渡すことで
Reactコンポーネント側でdispatchを使えるようにしています。
component側で使うようにプロパティの形にして渡してやる必要があります。

この時dispatchに使いたいactionCreatorを引数に渡してやります。

componentの項目であった

 

buttonタグのonClickを見てもらうとmapDispatchToPropsで作った
handleClickを呼び出していますね。こうすることでdispatchに渡されたactionCreatorが動き
reducerが仕事をしてくれて数値が反映されます。

 

Provider

後はindexで呼び出しているcomponent(Appコンポーネント)をProviderでラップしてあげるだけです。
もちろん、storeの項目でcreateStoreとして生成したstoreをProviderに渡すのを忘れないようにしてください。

// /index.js
import React from 'react';
import { render } from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';

import './index.css';

import App from './containers/App';
import reducer from './reducers/reducer'

let store = createStore(reducer);

let rootElement = document.getElementById('root')

render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
);

これでyarn start 等でデバッグモードで動かせばボタンを押して問題なく数値がプラスされていくはずです。
ちょっと駆け足になりましたが、コードを端折っているので1つずつ公開します。
コードやコメントの書き方が雑なのは許してください。 メインファイルのindex.js(store)は上記のモノで大丈夫です。


// /actions/action.js

/** -------------------------------------------------------------------
 * Action Creator
 -------------------------------------------------------------------*/ 
 // 役割:reducerが使用するactionを返すだけの単純な関数
 //--------------------------------------------------------------------
export function plusHogeProcess() {
  return {
    type: 'INC'
  }
}

// /reducers/reducer.js

/** -------------------------------------------------------------------
 * Reducer 
 --------------------------------------------------------------------*/ 
 // 役割:stateとactionを元に新しいstateを返す
 //--------------------------------------------------------------------

// ステート初期化用
const initialState = {
  hogehoge:1
}

// リデューサーを定義 
export default function reducer(state = initialState, action) {
  // 引数のアクションタイプ別で処理を切り替える。該当しなければ何もせずそのままstateを返す
  switch(action.type) {
    case 'INC':
      return {hogehoge: state.hogehoge + 1} //Object.assign({}, state,state.hogehoge + 1)
    case 'DEC':
      return Object.assign({}, state,state.hogehoge - 1)
    default:
      return state
  }
}

// /components/App.jsx

import React,{ Component } from 'react'
import logo from '../logo.svg'

/** -------------------------------------------------------------------
 * Component
 -------------------------------------------------------------------*/ 
 // 役割:表示用のReactコンポーネント
 //--------------------------------------------------------------------
export default class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" width="200px" />
        </header>
        <button onClick={() => this.props.handleClick()}>INC: { this.props.hogehoge }</button>

      </div>
    );
  }
}

// /containers/App.jsx

import { connect } from 'react-redux'
import { plusHogeProcess } from '../actions/action'
import App from '../components/App'

/** -------------------------------------------------------------------
 * Container
 -------------------------------------------------------------------*/ 
 // 役割:コンポーネントとストアをつなぐために使用(これも一応Reactコンポーネント)
 //--------------------------------------------------------------------
 const mapStateToProps = state => {
   const { hogehoge } = state
   return { hogehoge }
 }

 const mapDispatchToProps = dispatch => {
   return {
    handleClick: () => { dispatch(plusHogeProcess()) }
   }
 }

export default connect(mapStateToProps,mapDispatchToProps)(App)

以上になります。
殴り書きみたいになったのでまたそのうち整理して見やすいように修正しようと思います。

疲れた……