クモのようにコツコツと

フロントエンドエンジニア イイダリョウの技術ブログ。略称「クモコツ」

【React】Reduxを事始める(インストール〜ストア、レデューサー、プロバイダー設定)

Reactの続きです。前回は「コンテクスト(Context)」を使ってコンポーネントに値を直接渡しました。今回はReactの拡張ツール「Redux」を使って値の状態管理を事始めてみます。それではいきましょう!

【目次】

※参考:前回記事
【React】コンテクスト(Context)でネストされたコンポーネントに値を渡す - クモのようにコツコツと

※参考:Reactを習得するためにやったことまとめ
qiita.com

Reduxとは何か

今回も掌田さんの「React.js & Next.js入門」を参考に進める。

React.js & Next.js超入門

React.js & Next.js超入門

書籍ではコンポーネントの次には「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をインポート

まず冒頭でreduxreact-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文の条件にactiontypeを入れる
  • typePlusであれば連想配列を返す
    counterキーにstatecounter足す1を入れる
  • typeMinusであれば連想配列を返す
    counterキーにstatecounter引く1を入れる
  • それ以外はstateの初期値を返す

第1引数のstateに先程のステートcounterを読み込む。

第2引数のactionを用意すると、デフォルトでtypeプロパティが使える。これが分岐条件になる。(のちほどApp.jsで使う)

switch文はif文と違って並列な分岐条件。breakで処理を抜けるのが基本だが、returnも処理を抜けることができるのでbreakは不要。

※参考:【JSの基本-後編】書ける前に読む!HTML、CSS、JSの書式-5 - クモのようにコツコツと

ストアを作成

次にストアを作成する。

// ストア作成
let store = createStore(HitsyjiCounter);
  • 変数storecreateStore()を実行(引数はHitsyjiCounter

createStore()はReduxの組み込みメソッドで名前の通りストアをを作成する。引数で先程のHitsyjiCounterを読み込む。

※参考:createStore | Redux

プロバイダ

最後にをプロバイダー設定。ストアを他のコンポーネントに受け渡す仕組み。

// レンダリング
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
  • ProviderタグでAppタグを囲う
  • Providerタグのstore属性の値store

これでstoreの値がAppコンポーネントに渡される。

※参考:Provider | React Redux

App.jsの修正

次はApp.jsを修正する

react-reduxをインポート

Appコンポーネントでもreact-reduxをインポートする

import { connect } from 'react-redux';

ステートのマッピング

まずステート をマッピング(関連付け)する

// ステートのマッピング
function mappingState(state) {
  return state;
}
  • 関数mappingState()を設定(引数はstate
  • returnstateを返す

先程の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);
  • Appconnect()を作成し、引数Appにストアをコネクトする。

connect()は「react-redux」の組み込みメソッドでコンポーネントとストアを接続する。

Connect | React Redux

カッコが二つ続くのがあまり見かけない書き方!

これは関数の戻り値が関数のときに、戻り値の関数を作成すると同時に実行もする、という動きになる。

下記の記事の「かっこかっこ言ってる」を参照。

※参考:JavaScriptを読んでて「なにこれ!?」と思うけれど調べられない記法8選。 - Qiita

Messageコンポーネント

ステートを読み込む

次にAppコンポーネントに読み込まれているMessageコンポーネント部分

// メッセージのコンポーネント
class Message extends React.Component {
  render() {
    return (
    <div>
      <p>羊が{this.props.counter}匹</p>
    </div>
    );
  }
}
  • JSXレンダリング部分でpropscounterキーを読み込んで表示する
ストアのコネクト
// ストアのコネクト
Message = connect(mappingState)(Message);
  • Messageconnect()を作成(引数で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
  • ButtonClickDispatchButtonClickDispatchをバインド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
  • もしシフトキーを押したらpropsdispatch()を実行(typeMinusに)
  • さもなくばpropsdispatch()を実行(typePlusに)

シフトを押しながらクリックするとカウントがマイナスになる。普通のクリックはプラス。

「ディスパッチ」とはマルチタスクのプログラムに機能を割り当てることらしい

ディスパッチとは、複数のプログラムを実行中のマルチタスクオペレーティングシステムにおいて、プログラムに実行権を渡すことである。

※参考:ディスパッチとは (dispatch): - IT用語辞典バイナリ

これで関数実行のタイミングで条件によってtypeを変えてレデューサーの値を操作する。

レンダリング設定

JSXのレンダリング

  render() {
    return (
    <div>
      <button onClick={this.ButtonClickDispatch}>
        数える!
      </button>
    </div>
    );
  }
  • buttonタグでonClick属性を設定(値はButtonClickDispatch関数)

これでボタンを押したタイミングでButtonClickDispatch関数が実行される。

ストアのコネクト

最後にストアのコネクト

// ストアのコネクト
Button = connect()(Button);
  • Buttonconnect()を作成し、引数Buttonにストアをコネクトする。

ブラウザ動作

ターミナルでReactを起動する。

npm start

おお!ステートの値がちゃんと読み込まれて表示されている! f:id:idr_zz:20200623063727j:plain

「数える」ボタンをクリックすると… f:id:idr_zz:20200623063733j:plain 羊の数が増えた!

シフトを押しながらボタンを押すと… f:id:idr_zz:20200623063738j:plain 羊の数が減った!

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