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

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

本文書では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>
      <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で共有化したデータにアクセスできるところまで我慢して読み進めてください。

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;

Reduxでは上記のようにcreateStoreという関数でStoreを作成します。

この後に説明を行っていきますが、createStoreの引数にはreducerという関数が必須です。reducerはコンポーネント間で共有を行いたいデータを保持し、reducer関数の中でだけ唯一共有したデータを変更することができます。reducerという名前からは何を行うものなのか全くイメージが湧きませんがReduxで重要な役割を持ちます。

Reducerの作成

storeを作成するためにはreducerが必須となります。reducerは現在のデータの状態を保持するstateを持ちます。stateには初期値が必要となりreducer関数を実行すると必ず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コンポーネントに渡します。

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")
);

Providerコンポーネントは、ReactのコンポーネントからReduxのデータにアクセスするために必要な要素ですが、Providerだけではアクセスすることはできません。Reduxで共有化したデータにアクセスするためにconnect関数またはuseSelector Hookを利用します。

connect関数とuserSelectorの説明を行いますが、利用する方法はuseSelectorの方がわかりやすいと思います。

storeのcountへのアクセス

connet関数やuserSelectorを利用しなくてもstoreのデータにアクセスすることは可能なので簡単に説明しておきます。

storeをimportしてstore.getState().countを実行することでstoreに保存されているcountにアクセスすることもできます。


import "./App.css";
import store from './store/index'

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

export default App;
getStateを使ってstoreのcountを取得
getStateを使ってstoreのcountを取得

connect関数の利用

connect関数を利用してAppコンポーネント(App.js)からstoreに保管されているcountへのアクセスを行います。

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

mapStateToProps関数の中ではstateのどの値をpropsとしてコンポーネントに渡すのか中の処理で設定することができます。下記ではstateの設定したcountをcountという名前のpropsでAppコンポーネントに渡しています。


const mapStateToProps = state => {
  return { count: state.count }
}
stateという名前をつけていますが、任意の名前をつけることができます。

作成したmapStateToPropsの関数をconnectの引数に設定し以下のように記述する必要があります。mapStateToPropsはconnect関数からstoreのstateを渡されます。


export default connect(mapStateToProps)(App);

ここまでの設定を行ってようやくstoreに保管されているcountにコンポーネントからアクセスすることが可能となります。mapStatePropsで戻しているcountがAppのコンポーネントのpropsとして渡されていることも確認することができます。


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に置き換えることができるのでコードがすっきりします。connect関数ではpropsでstateの値を渡していましたがuseSelectorではpropsを利用しません。

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を利用

AppコンポーネントもuseSelectorで書き換えると下記のように表示されます。


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

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

export default App;

配列データの確認

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を使って配列データを表示

useSelectorではなくconnect関数とmapStateToPropsを利用した場合は下記のように記述することができます。


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

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

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

export default connect(mapStateToProps)(App);

先ほどmapStateToPropsの説明時に”stateのどの値をpropsとしてコンポーネントに渡すのか中の処理で設定することができます。”と言いましたが上記ではcount, postsをpropsとしてCountコンポーネントに渡していますがCountコンポーネントではpostsが必要でないのであればmapStateToPropsの中でpostsを戻す処理は必要ありません。

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をチェックすることでtypeによって異なる処理を行うことができます。先ほどまでのcountの例を使ってcountを1増やす処理と1減らす処理をreducerを使って行いたい場合は下記ように記述することができます。

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={increase}>Up</button>
  <button onClick={decrease}>Down</button>
</div>

increase, decreaseメソッドの中でdispatch関数を実行します。コンポーネントのpropsにdispatchが出てきましたこれがどこからきているのか気になるところです。connectの第2引数に何も指定していない場合にconnect関数はコンポーネントにpropsでdispatch関数(props.dispatch)を渡します。


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

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>
  );
}

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

export default connect(mapStateToProps)(App);

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

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

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

mapDispatchToPropsの利用

reducerにACTIONを伝えるためにdispatch関数を利用しましたがACTIONは他の方法でもreducerに伝えることができます。ここではmapDispatchToPropsとconnectを利用します。mapDispatchToPropsに変更してもブラウザ上動作は変わりません。mapDispatchToPropsを利用することでmapDispatchToPropsの中でclickイベントのメソッドを定義し、propsを利用して定義したメソッドをコンポーネントに渡します。mapDispatchToPropsを利用しない場合はclickイベントのメソッドはコンポーネントの中で定義していました。


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);
mapStateToProps、mapDispatchToPropsも名前の通りどちらもpropsを使ってコンポーネントにデータまたは関数を渡す時に利用するということを覚えておいてください。

useDispatch Hooksの利用

Storeのデータにアクセスした時にuseSelector Hooksが利用できたようにデータの変更に関するHooksもあります。変更についてはuseDispatch Hookを利用します。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の理解はできているかと思います。理解することと利用することは別のものなのでいろいろなネット上に落ちている例を使って理解を深めてください。下記のTodoリストのアプリケーションの作成も理解を深めるのに役に立ちます。