Reduxを利用することですべてのコンポーネントからアクセス可能なデータを一箇所で一元管理することができます。

Reduxは状態管理ライブラリ(State Management Library)と呼ばれています。状態管理という言葉ではどのような機能を持っているのかイメージすることが難しいので最初は”すべてのコンポーネントでデータを共有することができる仕組み”と考えた方がわかりやすいと思います。

本文書ではReduxで共有したデータへのアクセス方法と変更方法を学んでいきます。まずはRedux上でデータの共有化の設定を行い、データへのアクセス方法(Reduxで共有しているデータをブラウザ上で表示)を確認します。データへのアクセス方法を確認後にデータ変更方法を説明していきます。

環境の構築

Reduxの説明を行う前に今回利用するReactプロジェクトを作成します。任意の名前のReactプロジェクトを作成してください。ここではredux-learnにしています。


 % npx create-react-app redux-learn

作成したプロジェクトディレクトリに移動して、npmコマンドを実行してReactが起動するか確認を行ってください。


 % cd redux-learn
 % npm start
Reactの初期ページ
React初期ページ

インストール直後のフォルダ構成は下記の通りです。

インストール直後のフォルダ構成
インストール直後のフォルダ構成

本文書で利用しないファイル(logo.svg, App.test.js etc..)を削除しindex.jsファイルを更新します。

ファイルの削除やファイルの更新はReduxを利用するための必須な作業ではありません。読者の人がどの情報がReduxに影響を与えるか迷わせないため関連のない情報を可能なかぎり削除することを目的にしています。
ファイルの削除と更新
ファイルの削除と更新

index.cssファイルは下記のように更新します。


* {
  margin: 0;
  padding: 0;
}

App.cssの中身はすべて削除し、App.jsを更新します。


import React from "react";
import "./App.css";

function App() {
  return (
    <div className="App">
      <h1>Redux Learn</h1>
    </div>
  );
}

export default App;

ブラウザで確認するとApp.jsに記述したRedux Learnがブラウザの左上に表示されます。

更新後の画面
更新後の画面

これでReduxの動作確認するための環境構築は完了です。

Reduxデータへのアクセス方法

ReduxはReact専用のライブラリのように思うかもしれませんが、React専用ではなく他のフレームワークでも利用することが可能です。そのためReactでReduxを利用するためにはReduxのコアであるreduxライブラリとReactのコンポーネントからReduxにアクセスするためのreact-reduxのライブラリを利用します。

Reduxの設定については初めてであればアクセスするだけなのにこんなに手順が必要なのと感じるかと思います。またイメージのつきにくい単語がいくつも出てきますが慣れるまで我慢して読み進めてください。

Reduxのインストール

ReactでReduxを利用するためにはreactとreact-reduxをインストールする必要があります。npmコマンドでインストールを行ってください。


 % npm install redux react-redux

データ保管場所Storeの作成

アプリケーション全体で共有するデータを保管する場所Storeを作成します。

プロジェクトディレクトリにstoreディレクトリを作成しindex.jsファイルを作成してください。index.jsファイルの中でStoreの作成を行います。


import { createStore } from "redux";

const store = createStore();

export default store;

上記のようにcreateStoreという関数でStoreを作成しますが、引数にreducerという関数が必須です。reducerは共有を行いたいデータを保持し、reducer関数の中でだけ唯一共有したデータを変更することができます。reducerと名前から全くイメージが湧きませんがReduxで重要な役割を持ちます。

Reducerの作成

storeを作成するためにはreducerが必須となります。reducerは現在のデータの状態を保持するstateを持ちます。stateには現在の状態を保持するため初期値が必要となり関数を実行すると必ずstateを戻します。


const initialState = {
  count: 1,
};

const reducer = (state = initialState) => {
  return state;
};
reducer関数はstateの他に引数ととしてACTIONを指定します。ACTIONについてはデータの変更に関わるものなので後半のデータ変更の動作確認の際に説明を行います。

作成したreducerをcreateStoreの引数として設定します。これでreducerが保持しているstateをStoreの中に保管することができました。


import { createStore } from "redux";

const initialState = {
  count: 1,
};

const reducer = (state = initialState) => {
  return state;
};

const store = createStore(reducer);

export default store;

Storeにcountというデータを保存することができましたここまでの設定ではインストールしたreduxのライブラリのみ利用して行っています。

次はReactのコンポーネントからReduxのデータにアクセスする必要があるのでreact-reduxライブラリを使い設定を行っていきます。

Providerコンポーネント

ReactのコンポーネントからReduxのStoreにアクセスするためにはProviderコンポーネントが必要となります。index.jsファイルのルートコンポーネント<APP />をProviderコンポーネントで包みます。さらに作成したstoreをimportしてstoreをpropsとして渡します。

Providerでコンポーネントを包むことでreduxという独立した機能がreactと連携させているとイメージを持ってもらえるかと思います。


import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import { Provider } from "react-redux"
import store from "./store/index";

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

Prroviderを利用することでReactのコンポーネントからReduxのデータにアクセスすることが可能となります。

Providerを利用しただけではアクセスすることはできないのでアクセスするためにconnect関数を設定します。

Connect関数の利用

Appコンポーネント(App.js)からStoreに保管されているcountにアクセスを行います。

AppコンポーネントからReduxのStoreにアクセスするためにconnectを利用します。connectには引数としてmapStateToProps関数を指定します。mapStateToProps関数ではStoreの中で設定したstateをAPPコンポーネントで利用できる形のpropsへと変換(map)しています。

stateから取り出した値はAppコンポーネントのpropsとして渡すことができます。


const mapStateToProps = state => {
  return { count: state.count }
}

作成したmapStateToPropsの関数をconnectの引数に設定し以下のように記述する必要があります。


export default connect(mapStateToProps)(App);

ここまでの設定を行ってようやくStoreに保管されているcountにコンポーネントからアクセスすることが可能となります。


import React from "react";
import "./App.css";
import { connect } from "react-redux";

function App({ count }) {
  return (
    <div className="App">
      <h1>Redux Learn</h1>
      <p>Count: {count}</p>
    </div>:
  );
}

const mapStateToProps = (state) => {
  return { count: state.count };
};

export default connect(mapStateToProps)(App);

ブラウザから確認するとReduxのStoreで設定したcountが表示されます。

Storeのデータを表示
Storeのデータを表示

store¥index.jsファイルのinitialStateの値を1から100に変更するとブラウザで表示される値も100となります。Storeに保管したデータに間違いなくコンポーネントからアクセスしていることがわかります。


const initialState = {
  count: 100,
};
initialStateの値を変更
initialStateの値を変更

他のコンポーネントからのアクセス

AppコンポーネントからReduxのStoreのデータにアクセスすることができました。これだけではすべてのコンポーネントでデータを共有しているといいきることはできません。他のコンポーネントからも同様の方法でアクセスできるのか確認をしておきます。

srcディレクトリにcomponentsを作成し、countコンポーネントを作成します。Storeのデータのアクセス方法についてはAppコンポーネントで実行した方法と同じです。


import React from "react";
import { connect } from "react-redux";

function Count({ count }) {
  return (
    <>
      <div>Countコンポーネント:{count}</div>
    </>
  );
}

const mapStateToProps = (state) => {
  return { count: state.count };
};

export default connect(mapStateToProps)(Count);

作成したCountコンポーネントをAppコンポーネントでimportします。


import React from "react";
import "./App.css";
import { connect } from "react-redux";
import Count from "./components/Count";

function App({ count }) {
  return (
    <div className="App">
      <h1>Redux Learn</h1>
      <p>Count: {count}</p>
      <Count />
    </div>
  );
}

const mapStateToProps = (state) => {
  return { count: state.count };
};

export default connect(mapStateToProps)(App);

ブラウザで確認するとCountコンポーネントからもAppコンポーネントと同様の方法でアクセスできることが確認できます。APPコンポーネントでアクセスしたcountとCountコンポーネントでアクセスしたcountが表示されます。

Countコンポーネントからのアクセス
Countコンポーネントからのアクセス

useSelector Hooksを利用

ここまではReduxのデフォルトの機能(mapStateToProps, connect)を利用してReduxのデータにアクセスを行ってきましたが新たにReact Hooksが登場し、redux-reactのuseSelector Hooksを利用することができます。useSelectorを利用するとmapStateToPropsとconnectをuseSelectorに置き換えることができるのでコードが表示にすっきりします。。

CounterコンポーネントのみでuseSelectorを設定します。


import React from "react";
import { useSelector } from "react-redux";

function Count() {
  const count = useSelector((state) => state.count);
  return (
    <>
      <div>Countコンポーネント:{count}</div>
    </>
  );
}
export default Count;

結果は先ほどと変わりません。useSelectorのおかげでReduxのデータへのアクセス方法も簡単になったことが実感できると思います。

Countコンポーネントからのアクセス
useSelectorを利用

配列データの確認

countだけではシンプルすぎるのでReduxのStoreに配列データを保管した場合はどのような方法でブラウザに表示させるか確認を行っておきます。

store/index.jsファイルに配列postsを追加し、初期値を設定します。これでStoreの中にはcountとpostsが保管されることになります。どちらもコンポーネントからアクセスすることが可能です。


import { createStore } from "redux";

const initialState = {
  count: 50,
  posts: [
    { id: 1, title: "Reduxについて" },
    {
      id: 2,
      title: "ReduxのHooksについて",
    },
  ],
};

const reducer = (state = initialState) => {
  return state;
};

const store = createStore(reducer);

export default store;

Counterコンポーネントで追加したpostsデータをuseSelector Hookを利用して取得して展開します。postsのデータが取得できれば後はmap関数で展開します。


import React from "react";
import { useSelector } from "react-redux";

function Count() {
  const count = useSelector((state) => state.count);
  const posts = useSelector((state) => state.posts);
  return (
    <>
      <div>Countコンポーネント:{count}</div>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </>
  );
}
export default Count;

ブラウザ上では下記のように表示されます。Reduxで共有しているデータへのアクセス方法がわかれば後は通常通りの方法で表示させればいいことがわかりまます。

Reduxを使って配列データを表示
Reduxを使って配列データを表示

Reduxデータの変更方法

ここまでの説明でReduxデータへのアクセス方法を理解することができました。アクセスの次はどのように共有化したデータを変更するのかを確認していきます。

データを変更するためにはreducerのさらなる理解と新たにACTIONとdispatchにはどのような役割があるのかを理解する必要があります。

Reducerについて

Reduxのデータへのアクセスに注目していたためreducer関数の引数にはstateのみ指定し初期値を与えてそのまま初期値を持ったstateを戻すというものでした。ただコンポーネントで共有化したデータを見るだけではこのままで問題はありませんが、アプリケーションを構築するためには必ず共有化したデータの変更が必要となります。

Reducerの本来の目的は現在の状態であるstateとACTIONを受け取り、ACTIONで指示された内容でstateに変更を加え、新たな状態を作るというものです。

reducer関数の中でのみstoreに保存されたデータの変更を行うことができます。Storeのデータを変更したい場合はreducerを介して行わなければなりません。

Storeの中のデータにどのような変更を行いたいかをACTIONを使って指示を出しreducer関数がその指示を受け取って、reducer関数の中で変更の処理を行います。

ReduxのACTIONについて

ACTIONSとはtypeプロパティを持っているJavaScriptのオブジェクトです。


{
  type: 'INCREASE_COUNT'
}
ACTIONSはpayloadプロパティも持たせることができます。これについては後ほど説明します。payloadでreducerに対して値を渡すことができます。reducerはその受け取った値を使って処理を行います。

この形式をもったACTIONをreducerが受け取ってStoreのデータに対して変更を加えます。ACTIONはオブジェクトなのでACTION自体が何かの処理を行うことはありません。

ReducerとACTIONについて

Reducerはstateとactionを引数にとることができます。関数の中ではactionの内容によりstateに変更を加えて変更が加わった新しいstateを戻します。


const reducer = (state = initialState, action) => {
  //actionによりstateに変更を加えて変更が加わった新しいstateを戻す
  return state
}

変更する内容はACTIONのtypeによって異なるのでswitchによって分けることができます。action.typeがINCREASE_COUNTの場合はstateのcountの値を1増やし、action.typeがDECREASE_COUNTの場合はstateのcountの値を1減らします。何もaction.typeの指定がない場合はそのままstateを戻すというものです。


const reducer = (state = initialState, action) => {
  switch (action.type) {
    case "INCREASE_COUNT":
      return {
        count: state.count + 1,
      };
    case "DECREASE_COUNT":
      return {
        count: state.count - 1,
      };
    default:
      return state;
  }
};

ACTIONのtypeによりreducerでどのような処理を行うのが決れば最後ははどのようにACTIONをreducerに伝えるかです。その役割を持つものがdispatch関数です。

DispatchとACTION

dispatch関数の引数にはACTIONを指定することができます。ACTIONはtypeを持つオブジェクトなので下記のように記述することができます。


dispatch({ type: "INCREASE_COUNT" });

dispatch関数をメソッド内で実行することができればreducerに実行したいACTIONが伝わり、Storeの中のデータを変更することができます。

dispatchを実行させるためにAppコンポーネント内にクリックイベントを追加します。Upボタンをクリックするとincreaseメソッドが実行され、Downボタンを押すとdecreaseメソッドが実行されます。


<div className="App">
  <h1>Redux Learn</h1>
  <p>Count: {count}</p>
  <button onClick={increate}>Up</button>
  <button onClick={decreate}>Down</button>
</div>

increase, decreaseメソッドの中でdispatch関数を実行します。dispatch関数はpropsとしてApp関数が受け取っています。


function App({ dispatch, count }) {
  const increase = () => {
    dispatch({ type: "INCREASE_COUNT" });
  };
  const decrease = () => {
    dispatch({ type: "DECREASE_COUNT" });
  };
  return (
  <div className="App">
    <h1>Redux Learn</h1>
    <p>Count: {count}</p>
    <button onClick={increase}>Up</button>
    <button onClick={decreate}>Down</button>
  </div>
  );
}

ブラウザで確認するとUpとDownのボタンが表示されます。

Up, Downボタンの表示
Up, Downボタンの表示

UpボタンをクリックするとCount数が増え、DownボタンをクリックするとCount数が減ることを確認してください。ここまでの設定でStoreのデータの変更方法を理解することができました。

mapDispatchToPropsの利用

reducerにACTIONを伝えるためにdispatch関数を利用しましたがACTIONは他の方法でもreducerに伝えることができます。ここではmapDispatchToPropsとconnectを利用します。mapDispatchToPropsにブラウザ上での変更しても動作は変わりません。


import React from "react";
import "./App.css";
import { connect } from "react-redux";

function App({ count, increase, decrease }) {
  return (
    <div className="App">
      <h1>Redux Learn</h1>
      <p>Count: {count}</p>
      <button onClick={increase}>Up</button>
      <button onClick={decrease}>Down</button>
    </div>
  );
}

const mapStateToProps = (state) => {
  return { count: state.count };
};

const mapDispatchToProps = (dispatch) => {
  return {
    increase: () => dispatch({ type: "INCREASE_COUNT" }),
    decrease: () => dispatch({ type: "DECREASE_COUNT" }),
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(App);

useDispatch Hooksの利用

Storeのデータにアクセスした時にuseSelector Hooksが利用できたようにデータの変更に関するHooksもあります。useDispatch Hookで利用するためにはreact-reduxからimportする必要があります。

useSelectorを利用したCountコンポーネントでuseDispatchの設定を行います。connectを利用する必要があなくuseSelectorとuseDispatchを利用するとコードがすっきりします。


import React from "react";
import { useSelector, useDispatch } from "react-redux";

function Count() {
  const count = useSelector((state) => state.count);
  const dispatch = useDispatch();
  const increase = () => {
    dispatch({ type: "INCREASE_COUNT" });
  };
  const decrease = () => {
    dispatch({ type: "DECREASE_COUNT" });
  };
  return (
    <>
      <div>Countコンポーネント:{count}</div>
      <button onClick={increase}>Up</button>
      <button onClick={decrease}>Down</button>
    </>
  );
}
export default Count;

ここまで読み進めてStore, ACTION, Reducer, Dispatchが何でどのような役割をするのか答えることができればReduxの理解はできているかと思います。理解することと利用することは別のものなのでいろいろなネット上に落ちている例を使って理解を深めてください。