React Reduxについて


先日、Reduxに関する備忘録を書いたのですが、React Reduxに関しても学習したので、その備忘録としてQiitaにまとめてみました!

どこか間違いなどあれば、コメントにてご指摘頂けると助かります!

また、解説するコードの一部は、僕が学習に使用した教材を参考に自身で作成したアプリケーションの内容となっています。(以下のGithubリポジトリ内のコードを参照していただくと少しは記事の内容を追えるかと思います
アプリのGithubリポジトリ:https://github.com/TaikiYamano/React-Tweet-With-Redux

こちらの記事は以前投稿した、Reduxに関する記事とセットになっているので、こちらの記事をご一読されることをお勧めします

React Reduxとは

React Reduxとは、ReactとReduxを繋げる役割を持つものです。Reduxに関してまとめた記事でも説明したのですが、Reactでアプリを作成する際、Reduxを使用すると、Stateを別の場所で管理できて、どのコンポーネントからでもStateにアクセスできるようになります。こうすることで、ReactのコンポーネントはStateの管理をする必要が無くなります。

しかし、ReactとReduxを直接繋ぐと、Reactのコンポーネント内でStoreを操作する必要があります。そこで、ReactとReduxを繋ぐReact Reduxを使うことによって、ReactはStoreを操作する必要も無くなり、見た目の表示の役割に専念できるようになります。

また、React Reduxが行うReactとReduxを繋げる役割はコンテナーコンポーネント(Container Components)が行います。

React Redux(コンテナーコンポーネント)の具体的な役割

上記でReact ReduxはコンテナーコンポーネントでReactとReduxを繋げる役割を行うと説明しましたが、主に3つの役割でReactとReduxを繋いでいます。
1、StoreのStateが更新されたら、更新されたStateを取りにいく
2、Storeに取りに行ったStateの必要なものだけをReactのコンポーネントにProps経由で渡す
3、dispatchメソッドを使ってStoreのStateを更新する(更新されたStateは1、2の役割によってReactのコンポーネントに渡されます。)

コンテナーコンポーネントはReactとReduxの召使い的な役割のように見えますね笑

コンテナーコンポーネントの作成方法

ReactとReduxを繋げるコンテナーコンポーネントはReact Reduxが提供するconnectメソッドを用いて作成することができます。(公式ドキュメントによると、store.subscribe()を用いて手書きでも作成できるとのことですが、推奨はされていませんでした笑)

connectメソッドの第一引数にはmapStateToProps、第二引数にはmapDispatchToPropsを渡します。(第三引数にはmergeProps、第四引数にはoptionsを任意で渡すことができるらしいですが、今回の学習では使用しなかったので説明は省きます また学習した際にこの記事に追加しようと思います。)

以下は、簡易的なブログアプリ(Twitterのような)ものを作成した際に、投稿一覧を表示するためのコンポーネント(PostListコンポーネント)を作成し、ReactのPostListコンポーネントとReduxを繋げるコンテナーコンポーネントを作成した場合のコードです。

import { connect } from 'react-redux';

export default connect(
    mapStatetToProps,
    mapDispatchToProps
)(PostsList);

第一引数のmapStateToPropsと第二引数のmapDispatchToPropsに関しては後ほど説明します。
connectメソッドの返り値は関数なので、その引数にコンポーネント(今回はPostListコンポーネント)を渡すと、引数に渡したコンポーネントと結びつくコンテナーコンポーネント(今回の場合はPostListコンテナーコンポーネント)が作られます。
(こちらに関して、少し解釈が曖昧です...もし間違いがあればご指摘お願いします!

mapStateToProps

mapStateToPropsは、Storeのstateをprops経由でReactのコンポーネントに渡す際に、どのような形でコンポーネントに渡すかを定義する関数でオブジェクトを返します。また、第一引数にはStoreのStateを受け取ります。

因みにmapには「地図」と言う意味もありますが、IT用語で「何かと何かを関連づける」と言う意味も持つそうで、今回は後者の意味でmapが使われています。参考記事
なので、mapStateToPropsは「ReduxのStateとReactのPropsを関連づける」と言う意味になります。

以下は、自分が作った簡易的なブログアプリの機能の一つでStateのpostFilterで表示する投稿を変更しており、stateのpostFilterの値がLIKED_POSTSの場合、いいねが付いた投稿のみを表示する処理をmapStateToPropsに記述した際のコードです。

//react-reduxからconnectメソッドをインポート
import { connect } from 'react-redux';
//postFilterActionCreatorから定数LIKED_POSTをインポート
import {
    LIKED_POSTS
} from '../../actions/postFilterActionCreator';

const mapStatetoProps = (state) => {
    let posts;
//StateのpostFilterの値がLIKED_POSTSの場合、filterメソッドで全投稿(state.posts)の内、いいねフラグ(hasLiked)がtrueの投稿のみ、変数postsに格納する
    if(state.postFilter == LIKED_POSTS){
        posts = state.posts.filter(post => post.hasLiked());
    }else{
//StateのpostFilterの値がLIKED_POSTSでない場合、スプレッド構文でコピーした全投稿(state.posts)を変数postsに格納する
        posts = [...state.posts]
    }
    //変数postsを返す
    return { posts };
};

このmapStateTopropsをconnectの第一引数に渡すことで、Reactのコンポーネント内にてprops.postsと記述することで、Propsを経由してStoreのStateで管理している値(今回の場合はposts)にアクセスすることができます。
また、もしコンポーネント内でmapStateTopropsを定義していない場合、connectメソッドの第一引数にはnull、もしくはundefinedを渡します。

mapDispatchToProps

mapDispatchToPropsはどのようにしてStoreのStateを更新するかを定義する関数、もしくはオブジェクトです。mapDispatchToPropsが関数である場合は第一引数にdispatchを受け取ります。また、返り値はオブジェクトで、コールバック関数などが値としてセットされます。

因みに、mapDispatchToPropsは上記のmapの意味で「PropsとDispatchを関連づける」すなわち、PropsからStoreのStateをDispatchで更新すると言う意味になります。

以下は自分が作成した簡易的なブログアプリで投稿(ツイート)に「投稿者名」「本文」「画像」の情報を記述して投稿する処理をmapDispatchToPropsに関数をセットして記述した際のコードです。

import { connect } from 'react-redux';
//postActionCreatorからcreatePostをインポート
import { createPost } from '../../actions/postActionCreator'

const mapDispatchToProps = (dispatch) => {
    return {
//返り値のオブジェクトにコールバック関数をセット
        createPost: (name,text,image) => {
//変数actionにcreatePostアクションによって作られたActionを格納
            const action = createPost(name,text,image);
//dispatchメソッドの引数に変数actionを渡して、StoreのStateを更新
            dispatch(action);
        }
    }
};

また、mapDispatchToPropsにオブジェクトをセットした場合、上記のコードは以下のようになります。

import { connect } from 'react-redux';
import { createPost } from '../../actions/postActionCreator'

const mapDispatchToProps = { createPost };

この記述で関数をセットした場合と同じ処理を行います。こちらの方が処理の追いやすさは上記のコードよりは劣りますが、コードの量自体は大幅に減るのでいいですね。

そして、このmapDispatchToPropsをconnectメソッドの第二引数に渡すことによって、Reactのコンポーネント内にて props.createPost(name,text,image)と記述することで、Props経由でStoreのStateを更新することができます。(dispatch(action)を通じてコンテナーコンポーネントがStoreのStateを更新してくれている為です。)

また、もしコンポーネント内でfを定義していない場合、connectメソッドの第二引数にはnullを渡します。

コンテナーコンポーネントをApp.jsに記述する

先程説明しましたが、connectメソッドの引数にmapStateToPropとmapDispatchToPropsを渡し、connectメソッドの返り値になる関数の引数にコンポーネントを渡すとコンテナーコンポーネントを作成することができます。

以下は投稿一覧を表示するPostListコンテナーコンポーネントを作成した場合のコードです


import { connect } from 'react-redux';

export default connect(
    mapStatetToProps,
    mapDispatchToProps
)(PostsList);

このコンテナーコンポーネントは以下のように、普通のコンポーネントと同じようにApp.js内に記述することができます。
(以下のコードはもう少し色々記述していましたが、できるだけシンプルにするよう大幅に省略しています

App.js
import React from 'react';
import PostList from '../src/components/PostsList/PostsList';

function App() {
  return (
    <div className="App">
      <PostList />
    </div>
  );
}

export default App;

しかし、このままの記述だとアプリを立ち上げた際に以下のようなコンテナコンポーネントにてStoreが見つからないと言うエラーが発生します。

このエラーを解決するにはindex.jsにReact Reduxが提供しているProviderを記述する必要があります。
Providerによってネストされた全てのコンテナーコンポーネントはReduxのStoreにアクセスすることができるようになるからです。

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
//react-reduxからProviderをインポート
import { Provider } from 'react-redux';
//ProviderコンポーネントのPropsであるstoreにセットするstoreをインポートする
import store from './store/index';
ReactDOM.render(
//AppコンポーネントをProviderコンポーネントでネストする。Propsであるstoreには上記でインポートしたstoreをセットする
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

こうすることで先程発生したエラーは出なくなります。

参考記事

React Reduxの学習をするにあたり、使用した教材の他に以下の記事を参考にさせて頂きました!

React + Redux の基本的な使い方
React Redux の難しかった点をできるだけシンプルに図解