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

アクセス可能なデータを一元管理??Reactの初心者の方であればデータを一元管理するということがどういうことを言っているのかも理解し難いかもしれません。Reactでは複数のコンポーネントを構成してアプリケーションを構築するためコンポーネント間でデータの受け渡しが必ず必要になります。コンポーネント間でデータを渡す場合は親コンポーネントから子コンポーネントへpropsを使って行います。しかしpropsでは親子関係のないコンポーネント間ではデータを渡すことができません。また親と子のみの関係であればpropsを一度渡すだけでいいのですが子コンポーネントにさらに子コンポーネントがあるとpropsをバケツリレーのように渡していなかければなりません。どのコンポーネントからもアクセス可能なデータを一元管理する場所があればバケツリレーの問題を解消することができます。データを一元管理するために利用するライブラリがReduxです。Reduxを利用すればどのコンポーネントからも同じ方法で共有したデータにアクセスすることが可能になります。propsでは親から渡されたデータを子コンポーネントで更新することはできませんがReduxであればどのコンポーネントからも共有したデータを更新することが可能になります。

Reduxなし、あり
Reduxを使っていない場合と使っている場合の図

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

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

Reactを習い始めたばかりの人であればReduxを使う場面に直面したからではなくReduxというものが存在することを知って学習し始めた場合、Reduxを利用するために記述するコードと意味不明な単語が多くてなかなか身につかないかもしれません。本文書を読んでも設定する方法は理解できても利用する意味がわからないかもしれません。Reactを利用してアプリケーションを構築している時にpropsを利用することなくどのコンポーネントからもアクセスする方法ないのかな。と思った時がReduxの学習を開始する一番いい時期かもしれません。
fukidashi

ReduxはRedux Toolkitを利用することが公式サイトでも推奨されていますが本書を理解することでReduxの理解を深めることができます。

Redux以外の状態管理ライブラリは複数存在しますがZustandもおすすめです。

環境の構築

Reduxの説明を行う前に動作確認を行う環境を構築するためにViteを利用してReactプロジェクトの作成を行います。”npm create vite@latext”コマンドを実行するとプロジェクト名、フレームワーク、Variantを聞かれるので”redux-learn”, “React”, “JavaScript”を選択します。プロジェクト名は任意の名前をつけてください。


% npm create vite@latest
Need to install the following packages:
create-vite@5.4.0
Ok to proceed? (y) y


> npx
> create-vite

✔ Project name: … redux-learn
✔ Select a framework: › React
✔ Select a variant: › JavaScript

Scaffolding project in /Users/mac/Desktop/redux-learn...

Done. Now run:

  cd redux-learn
  npm install
本文書ではnpmコマンドを利用していますがyarn, pnpmコマンドを使うことも可能です。
fukidashi

作成したプロジェクトディレクトリに移動して、”npm install”コマンドを実行してJavaScriptライブラリのインストールを行い、npm run devコマンドで開発サーバの起動を行います。


 % cd redux-learn
 % npm install
 % npm run dev

> redux-learn@0.0.0 dev
> vite
  VITE v5.3.4  ready in 724 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

http://localhost:5173にブラウザからアクセスすると下記の初期画面が表示されます。

Vite+Reactのトップページ
Vite+Reactのトップページ

デフォルトで設定されているスタイルを解除するためにsrcディレクトリのmain.jsxファイルを開いてindex.cssのimportをコメントします。


import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
// import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

srcディレクトリのApp.jsxファイルを以下のコードに更新します。


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

export default App;

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

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

これでReduxの動作確認するための環境構築は完了です。ここからReduxの動作確認を行っていきます。

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

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

Reduxの設定を初めて行う人であれば共有化したデータにアクセスするだけなのにこんなにわけのわからない手順が必要なのと思うはずです。また機能のイメージと関連させるのが難しい単語がいくつも出てくるためなぜそんな設定が必要なのかという疑問も出てきます。最初はあまり考えすぎずReduxで共有化したデータにアクセスできるところまで我慢して読み進めてください。

Reduxのインストール

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


 % npm install redux react-redux

インストールが完了したらpackage.jsonファイルでインストールされたライブラリとバージョンを確認することができます。Reactのバージョンは18, reduxのバージョンは5であることがわかります。


{
  "name": "redux-learn",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-redux": "^9.1.2",
    "redux": "^5.0.1"
  },
  "devDependencies": {
    "@types/react": "^18.3.3",
    "@types/react-dom": "^18.3.0",
    "@vitejs/plugin-react": "^4.3.1",
    "eslint": "^8.57.0",
    "eslint-plugin-react": "^7.34.3",
    "eslint-plugin-react-hooks": "^4.6.2",
    "eslint-plugin-react-refresh": "^0.4.7",
    "vite": "^5.3.4"
  }
}

データ保管場所Storeの作成

Reduxではアプリケーション全体で共有するデータを保管する場所が必要になります。その場所はstoreと呼ばれ、アプリケーションを構成するコンポーネントとは全く独立した場所に作成します。storeの中に共有を行うデータstateが保存されます。

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


import { createStore } from "redux";

const store = createStore();

export default store;

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

Reducerの作成

storeを作成するためにはreducerが必須となります。reducerは引数に現在のデータの状態を保持するstateを持ちます。引数のstateには初期値が必要となりreducer関数を実行すると必ずstateを戻します。reducerの関数の中でstateの変更を行う処理を追加することで変更が反映された新しいstateが戻されることになります。


const initialState = {
  count: 1,
};

const reducer = (state = initialState) => {
  return state;
};
reducer関数はstateの他に引数としてActionを指定します。Actionはデータの変更の指示が含まれるオブジェクトで後半のデータ変更の動作確認の際に説明を行います。Actionに書かれた指示に従ってreducerがstateに変更を加えます。
fukidashi

作成した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;

ここまでの設定ではインストールしたreduxのライブラリのみ利用してstoreにcountというデータを保存することができました次はReactのコンポーネントからReduxのデータにアクセスする必要があるのでreact-reduxライブラリを使い設定を行っていきます。

Providerコンポーネント

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

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


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

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

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

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

storeにあるstateへのアクセス

storeの中にあるstateデータにアクセスすることだけであれば、storeをimportしてstore.getState().countを実行することでstoreに保存されているstateのcountにアクセスすることができます。getStateを実行するとcountだけではなくstoreに保管されているすべてのstateの現在の状態を確認することができます。しかしstateの変更を行なってもブラウザ上に即座に変更が反映されるわけではありません。getStateは実行時の最新のstateの情報を取得することができます。


import store from './store/index';

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

export default App;

ブラウザで確認するとstore/index.jsファイルで設定したcountの値の1を確認することができます。

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という名前をつけていますが、任意の名前をつけることができます。
fukidashi

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


export default connect(mapStateToProps)(App);

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


import { connect } from 'react-redux';

function App({ count }) {
  return (
    <div>
      <h1>Redux Learn</h1>
      <p>Count: {count}</p>
    </div>
  );
}
const mapStateToProps = (state) => {
  return { count: state.count };
};
export default connect(mapStateToProps)(App);

// export default App;

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

getStateを使ってstoreのcountを取得
connectを利用して表示したcountの値

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


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

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

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

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


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.jsxファイルでimportします。


import { connect } from 'react-redux';
import Count from './components/Count';

function App({ count }) {
  return (
    <div>
      <h1>Redux Learn</h1>
      <p>Count: {count}</p>
      <Count />
    </div>
  );
}
const mapStateToProps = (state) => {
  return { count: state.count };
};
export default connect(mapStateToProps)(App);

// export default 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に置き換えることができるのでコードがすっきりします。selectという単語がuseSelectorには含まれているとおりstoreに保存されているstateデータの中から必要なデータを選択して取り出すことができます。connect関数ではpropsでstateの値を渡していましたがuseSelectorではpropsを利用しません。

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


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 { useSelector } from "react-redux";

function App() {
  const count = useSelector((state) => state.count);
  return (
    <div>
      <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;

Countコンポーネントで追加したpostsデータをuseSelector Hookを利用して取得して展開します。選択するデータcountと異なるだけで選択方法は同じです。postsのデータが取得できれば後はmap関数で展開します。


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;

Appコンポーネントでは下記を記述しています。


import { useSelector } from 'react-redux';
import Count from './components/Count';

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

export default App;

ブラウザ上では下記のように表示されます。Reduxで共有しているデータへアクセスし取得することができればReduxを利用しない場合と同様の方法(map関数など)で表示させればいいことがわかりまます。

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

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


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コンポーネントに渡しています。Appコンポーネントではpostsが必要ないのであればmapStateToPropsの中でpostsを戻す処理は必要ありません。

getStateによるstateの状態を確認

getStateメソッドによりstateにアクセスすることが可能だということを先ほど説明しました。stateの状態を確認するためにgetStateを利用することができます。


import { createStore } from 'redux';
//略
const store = createStore(reducer);
console.log(store.getState());

export default store;

ブラウザのデベロッパーツールのコンソールを確認するとstateの状態が表示されます。countとpostsなどstateに含まれるすべての値を確認することができます。

getStateメソッドによるstateの確認
getStateメソッドによるstateの確認

combineReducersの設定

先ほどのコードでは1つのreducerの中にcountとpostsを設定していしたが、目的や用途によって複数のreducerに分けることができます。count、postsをそれぞれcountReducer, postsに分けます。


const countReducer = (
  state = {
    count: 50,
  }
) => {
  return state;
};

const postsReducer = (
  state = {
    posts: [
      { id: 1, title: 'Reduxについて' },
      {
        id: 2,
        title: 'ReduxのHooksについて',
      },
    ],
  }
) => {
  return state;
};
ここでは1つのファイルの中に複数のreducerを記述していますが通常はreducerごとにファイルを分けexportを行い、index.jsファイルでimportを行います。
fukidashi

分けたreducerを一緒に利用するためにcombineReducersを利用します。combineReducersの引数のオブジェクトには作成したreducerを設定します。


const rootReducer = combineReducers({
  countReducer,
  postsReducer,
});

combineReducersで実行後の値をrootReducerに保存してcreateStoreの引数に指定します。


import { createStore, combineReducers } from 'redux';

const countReducer = (
  state = {
    count: 50,
  }
) => {
  return state;
};

const postsReducer = (
  state = {
    posts: [
      { id: 1, title: 'Reduxについて' },
      {
        id: 2,
        title: 'ReduxのHooksについて',
      },
    ],
  }
) => {
  return state;
};

const rootReducer = combineReducers({
  countReducer,
  postsReducer,
});

const store = createStore(rootReducer);
console.log(store.getState());

export default store;

useSelector Hookのstate.counterからstate.countRecucer.countに変更することでcountの値をブラウザ上に表示させることができます。


import { useSelector } from 'react-redux';

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

export default App;

store.getStateの結果をブラウザで確認します。reducersを分割する前とはgetStataの状態は変わりますがcountもpostsもstateの中に保存されていることが確認できます。

combineReducersを利用した場合のgetState
combineReducersを利用した場合のgetState

Reduxデータの変更方法

ここまでの説明でStoreに保存されているstateデータへのアクセス方法を理解することができました。次は共有化したデータstateの変更方法について確認していきます。

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

Reducerについて

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

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

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

storeの中のデータにどのような変更を行いたいかをActionを使って指示を出しreducer関数がその指示を受け取って、reducer関数の中で変更の処理を行います。言葉の説明だけでは理解することは難しいので実際にコードを見ながら確認していきます。

ReduxのActionについて

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


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

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

Action Creatorsとは

Reduxの情報を調べていくとActionだけではなくAction Creatorsという言葉を目にします。これはActionを作成する関数です。実行するとActionのオブジェクトを戻します。Action Creatorsは必須のものではないので必ず利用しなければならないといったものではありません。言葉は出てくるので使わなくてもどのようなものか理解はしておきましょう。


export function increase(payload) {
  return {
    type: 'INCREASE_COUNT',
    payload: payload
  }
}

ReducerとActionについて

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


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

先ほど説明した通りActionはtypeプロパティを持つオブジェクトでaction.typeをチェックすることでtypeによって異なる処理を行えるように設定を行います。typeによって異なる処理を行わせるためにswitch関数を使って分岐させます。先ほどまでのcountの例を使ってcountを1増やす処理と1減らす処理をreducerを使って行いたい場合は下記ように記述することができます。


import { createStore } from 'redux';

const initialState = {
  count: 50,
};

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

const store = createStore(reducer);

export default store;

action.typeがINCREASE_COUNTの場合はstateのcountの値を1増やし、action.typeがDECREASE_COUNTの場合はstateのcountの値を1減らします。action.typeの指定がない場合はそのままstateを戻すというものです。

Actionのtypeによりreducerでどのような処理を行うかを設定した後はActionをreducerに伝える方法が必要となります。Actionをreducerに伝えるための方法がdispatch関数です。

DispatchとAction

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


dispatch({ type: "INCREASE_COUNT" });

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

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


import { connect } from "react-redux";

function App({ dispatch, count }) {
  const increase = () => {
    dispatch({ type: "INCREASE_COUNT" });
  };
  const decrease = () => {
    dispatch({ type: "DECREASE_COUNT" });
  };
  return (
  <div>
    <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);

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

ブラウザで確認すると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 { connect } from "react-redux";

function App({ count, increase, decrease }) {
  return (
    <div>
      <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を使ってコンポーネントにデータまたは関数を渡す時に利用するということを覚えておいてください。
fukidashi

useDispatch Hooksの利用

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

App.jsxファイルを以下のように更新します。connectを利用する必要がなくuseSelectorとuseDispatchを利用するとコードがすっきりします。


import { useSelector, useDispatch } from 'react-redux';

function App() {
  const count = useSelector((state) => state.count);
  const dispatch = useDispatch();
  const increase = () => {
    dispatch({ type: 'INCREASE_COUNT' });
  };
  const decrease = () => {
    dispatch({ type: 'DECREASE_COUNT' });
  };
  return (
    <div>
      <h1>Redux Learn</h1>
      <p>Count: {count}</p>
      <button onClick={increase}>Up</button>
      <button onClick={decrease}>Down</button>
    </div>
  );
}

export default App;

非同期の処理

外部のサーバにアクセスを行い取得したデータをブラウザ上に表示したい場合にreduxではどのように設定を行うか確認をしていきます。まずはreduxを利用せず取得したデータを表示させる方法を確認します。

Reduxを利用しない場合

Postコンポーネントを作成するためにcomponentsフォルダにPost.jsファイルを作成します。データを取得する外部サーバにはJSONPLACEHolderを利用しています。https://jsonplaceholder.typicode.com/postsにアクセスすると100件分のデータを取得することができます。useEffect Hookの中で非同期のgetPostsメソッドを実行して取得したデータを変数postsに保存しています。map関数を利用してpostsの中身を展開しタイトルのみ表示させています。


import { useState, useEffect } from 'react';

const Post = () => {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    const getPosts = async () => {
      const res = await fetch('https://jsonplaceholder.typicode.com/posts');
      const data = await res.json();
      setPosts(data);
    };
    getPosts();
  }, []);

  return (
    <div>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
};

export default Post;

作成したPost.jsxファイルをApp.jsxファイルでimportを行います。


;import Post from './components/Post';

function App() {
  return (
    <div>
      <h1>Redux Learn</h1>
      <Post />
    </div>
  );
}

export default App;

ブラウザ上に100件のPOSTデータのタイトルが表示されます。

Reduxを利用した場合

Reduxを利用した場合はstoreに取得したデータを保存するためにstore/index.jsファイルでpostsの初期値の設定とreduceの処理を追加します。ActionにGET_POST_DATAを追加しtypeがGET_POST_DATAの場合にpayloadの値をpostsに設定します。


import { createStore } from 'redux';

const initialState = {
  posts: [],
};

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case 'GET_POST_DATA':
      return { ...state, posts: action.payload };

    default:
      return state;
  }
};

const store = createStore(reducer);

export default store;

reducerとActionの設定が完了したらPost.jsxファイルの更新を行います。dispatchを利用するuseDispatch、postsをstoreから取得するためにuseSelector Hookをimportします。

getPostメソッドの中で外部サーバからデータを取得した直後にdispatchを実行し、payloadには取得したデータdataを設定しています。


import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';

const Post = () => {
  const dispatch = useDispatch();
  const posts = useSelector((state) => state.posts);

  useEffect(() => {
    const getPosts = async () => {
      const res = await fetch('https://jsonplaceholder.typicode.com/posts');
      const data = await res.json();
      dispatch({
        type: 'GET_POST_DATA',
        payload: data,
      });
    };
    getPosts();
  }, [dispatch]);

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
};

export default Post;

ブラウザ上に100件分のデータ表示されれば非同期でのReduxの処理の設定も正常に動作しています。

Reduxで非同期の処理
Reduxで非同期の処理

redux-thunkを利用した非同期処理

ミドルウェアのredux-thunkを利用するためにはパッケージのインストールが必要です。redux-thunkは非同期の処理を行いたい時に活用することができます。


 % npm install redux-thunk

redux-thunkはapplyMiddlewareを利用して設定を行います。


import { createStore, applyMiddleware } from 'redux';
import { thunk } from 'redux-thunk';

//略

const store = createStore(reducer, applyMiddleware(thunk));

export default store;

getPosts関数をstore/index.jsファイルの中に追加していますがgetPosts関数の中では引数にdispatchを持つ関数を戻していることがわかります。またgetPosts関数の中でdispatch関数により同期処理のAction GET_POST_DATAを実行しています。


export const getPosts = () => {
  return async (dispatch) => {
    const res = await fetch('https://jsonplaceholder.typicode.com/posts');
    const data = await res.json();
    dispatch({
      type: 'GET_POST_DATA',
      payload: data,
    });
  };
};
動作確認のコードなのでgetPost関数をstore/index.jsファイルの中に記述してますが別ファイルに記述しても問題はありません。
fukidashi

getPosts関数はexportを行なっているのでPost.jsファイルでimportを行いuseEffect Hookの中で実行します。Redux Thunkを利用した場合はdispatchの引数に関数を戻す関数(getPost())を指定することができます。通常はdispatchの引数に関数を指定した場合はActionのオブジェクトを戻します。


import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { getPosts } from '../store/index';

const Post = () => {
  const dispatch = useDispatch();
  const posts = useSelector((state) => state.posts);

  useEffect(() => {
    dispatch(getPosts());
  }, [dispatch]);

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
};

export default Post;

ブラウザで確認すると100件のPOSTデータが表示されるはずです。先ほどと同様にRedux Thunkを利用しても非同期の処理が行えることが確認できます。

Redux Devtoolsの利用

Reduxの現在のStateの状態やACTIONの履歴を確認したい場合にRedux DevToolsを利用することができます。Redux DevToolsを利用するためにブラウザのExtentionのインストールが必要になります。

Redux Devtools
Redux Devtools

ブラウザでDevtoolsをインストール後にDevtoolsが利用できるようにstore/index.jsファイルで設定を行います。


const store = createStore(
  reducer,
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

ブラウザのデベロッパーツールを開くとタブの中にReduxがあることが確認できます。Reduxとすると以下の画面が表示されます。左側のパネルにはGET_POST_DATAのACTIONが表示されています。右側のパネルの上部にAction, State, Diffが見えますがStateをクリックすると現在のstateの状態を確認することができます。

Redux Devtoolsの表示
Redux Devtoolsの表示

その他

ここまで読み進めてStore, Action, Reducer, Dispatchが何でどのような役割をするのか答えることができればReduxの理解はできているかと思います。理解することと利用することは別のものなのでいろいろなネット上に落ちている例を使って理解を深めてください。下記のTodoリストのアプリケーションの作成も理解を深めるのに役に立ちます。