1行づつ丁寧に読む初心者のRedux


プログラミング歴半年の素人が書いています。

間違いのないようご自身でも良く調べた上でお願いいたします。

対象読者

  • モバイルアプリ開発をしたことがない人
  • JavaScriptの基礎構文がわかる人
  • Reactを知っている人
  • Macの人

わたしです。 試行錯誤の記録となっておりますので、ベストプラクティスではない可能性が十分含まれておりますことご了承ください。

そもそも何故Reduxが必要なのか

Reactでは、コンポーネントとコンポーネントの間でデータのやりとりをする場面が多々あります。
そんな時に使うのが「State」や「Props」です。
(Reduxについて調べている人は、Reactの基礎的な知識をお持ちかと思いますので「State」「Props」の説明は省きます。)

データは上から下に流れる

例えばPropsは親コンポーネントから子コンポーネントへと流れて行きます。

これは親と子だけなら分かりやすいのですが、実際には孫や兄弟が存在します。

  • 「child-1」から「child-2」へデータを渡したい時
  • 「parent」から「grand-child」にデータを渡したい時

など、データの流れは複雑になり、間違いが起きやすくなってしまいます。

+-------------------------------------+
|                                     |
| parent component                    |
|                                     |
+-------+--------------------+--------+
        |                    |
        |                    |
+-------v------+      +------v--------+
|              |      |               |
| child-1      +--X   | child-2       |
|              |      |               |
+-------+------+      +---------------+
        |
        |
        |                XXX 上には
+-------v---------+       |  行けない!
|                 |       |
| grand-child     +-------+
|                 |
+-----------------+

だったらデータ専用の保管庫を作っておこう!

というのがRedux(Flux)の考え方です。

全てのデータをStateとして専用の保管庫「Store」に入れておき、どのコンポーネントからでもアクセスする事ができます。
ん?それって所謂「グローバル」ってことかも。

Stateの事ならとにかくStoreに問い合わせればOK! という事です。

+------------------+       +-------+
| parent           <------->XXXXXXX|
|                  |       |XXXXXXX|
+------------------+       |XXXXXXX|私
                           |       |が
        +------------------> Store |全
        |                  |       |て
+-------v-+ +---------+    |XXXXXXX|を
|         | |         |    |XXXXXXX|司
| child-1 | | child-2 <---->XXXXXXX|る
|         | |         |    |XXXXXXX|
+---------+ +---------+    |XXXXXXX|
                           |XXXXXXX|
+-------------+            |XXXXXXX|
|             |            |XXXXXXX|
| grand-child <------------>XXXXXXX|
|             |            |XXXXXXX|
+-------------+            +-------+

Reduxを使ってみる

今回はReact-Nativeを使っていますが、ReactでもOKです。
本来はファイルを分けますが、ひとまず1ファイルでReduxの流れを確認してみます。

全体の流れを想像してみる

まず、ここにルート直下のindex.jsがあります。
このファイルはただAppコンポーネントをレンダリングしているだけで何も特別な動きはありません。

index.js
import {AppRegistry} from 'react-native';
import App from './src/views/containers/App';
import {name as appName} from './app.json';

AppRegistry.registerComponent(appName, () => App);

そこに「Store」という保管庫が(あったらいいなあ...)と想像しました。

index.js
import {AppRegistry} from 'react-native';
import App from './src/views/containers/App';
import {name as appName} from './app.json';

//Store(グローバルなStateの保管庫)があったらいいなぁ...

AppRegistry.registerComponent(appName, () => App);

Stateはいじるためにあります。変化させるものです。Stateをどんな風にいじるか、「Action」を決めておきたいと思いました。 今回はStateのカウンタを+1させる事にしました。

index.js
import {AppRegistry} from 'react-native';
import App from './src/views/containers/App';
import {name as appName} from './app.json';

//Store:(グローバルなStateの保管庫)があったらいいなぁ...

//Action: Stateの数字に +1したい!

AppRegistry.registerComponent(appName, () => App);

さらに確実にStateをいじるために、いじり方を具体的に決める「Reducer」という仕組みを思いつきました。

index.js
import {AppRegistry} from 'react-native';
import App from './src/views/containers/App';
import {name as appName} from './app.json';

//Store:(グローバルなStateの保管庫)があったらいいなぁ...

//Action: Stateの数字に +1したい!

//Reducer: Actionをさらに具体的にするぜ!

AppRegistry.registerComponent(appName, () => App);

そして最後の実行者「Dispatch」が登場しました。決定したいじり方で、Stateを実際に変化させます。

index.js
import {AppRegistry} from 'react-native';
import App from './src/views/containers/App';
import {name as appName} from './app.json';

//Store:(グローバルなStateの保管庫)があったらいいなぁ...

//Action: Stateの数字に +1したい!

//Reducer: Actionをさらに具体的にするぜ!

//Dispatch: 決めた内容に沿って実行いたします。

AppRegistry.registerComponent(appName, () => App);

想像を実装してみる

Storeを作るのは複雑なので、まずは Actionから作っていきます。

Actionは簡単です。オブジェクトを返すだけのシンプルな関数にしておきます。
オブジェクトには必ずtype:を付けて、どんなアクションのタイプか間違わないようにしておきます。

index.js
import {AppRegistry} from 'react-native';
import App from './src/views/containers/App';
import {name as appName} from './app.json';

//Store:(グローバルなStateの保管庫)があったらいいなぁ...

//Action: Stateの数字に +1したい!
const plusOne = () => (
  {type: 'PLUS_ONE'}
)

//Reducer: Actionをさらに具体的にするぜ!

//Dispatch: 決めた内容に沿って実行いたします。

AppRegistry.registerComponent(appName, () => App);

ついでに、「マイナス1する」アクションも作っておきます。ほぼ「プラス1」するアクションのコピーですね。

index.js
import {AppRegistry} from 'react-native';
import App from './src/views/containers/App';
import {name as appName} from './app.json';

//Store:(グローバルなStateの保管庫)があったらいいなぁ...

//Action: Stateの数字に +1したい!
const plusOne = () => (
  {type: 'PLUS_ONE'}
)
const minusOne = () => (
  {type: 'MINUS_ONE'}
)

//Reducer: Actionをさらに具体的にするぜ!

//Dispatch: 決めた内容に沿って実行いたします。

AppRegistry.registerComponent(appName, () => App);

これでアクションの実装は終わりです。{type: 〇〇}を返すだけの関数が2個できました。

では、このアクションをさらに具体的に決定していく「Reducer」を作っていきます。
「Reducer」の引数には、実際に変化させたいStateを指定したいのですが、まだ、Stateの保管場所である「Store」を作っていません。なので、今は仮で引数にStateを直接書き込んでいます。(Stateの初期値はひとまずゼロ)

index.js
//...省略

//Action: Stateの数字に +1したい!
const plusOne = () => (
  {type: 'PLUS_ONE'}
)
const minusOne = () => (
  {type: 'MINUS_ONE'}
)

//Reducer: Actionをさらに具体的にするぜ!
const counter = (state = 0, action) => {
  switch (action.type) { //actionのtypeによって
    case 'PLUS_ONE':
      return state + 1;  // +1 したり
    case 'MINUS_ONE':
      return state - 1;  // -1 したりする
  }
}

これで「Action」と「Reducer」ができました!

実は、Reduxのすごいところは、今作った「Reducer」から「Store」を自動で作ってしまうのです。

index.js
//..省略

//Reducer: Actionをさらに具体的にする
const counter = (state = 0, action) => {
  switch (action.type) { //actionのtypeによって
    case 'PLUS_ONE':
      return state + 1;  // +1 したり
    case 'MINUS_ONE':
      return state - 1;  // -1 したりする
  }
}

//Reducerから必要なStateを自動判定して、Storeを作る事ができる!
let store = createStore(counter);

これで準備は整いました。実行者「Dispatch」に、アクションを実行してもらいましょう!

index.js
// ..省略

//Action: Stateの数字に +1 か -1 したい!
const plusOne = () => (
  {type: 'PLUS_ONE'}
)
const minusOne = () => (
  {type: 'MINUS_ONE'}
)

//Reducer: Actionをさらに具体的にするぜ!
const counter = (state = 0, action) => {
  switch (action.type) { //actionのtypeによって
    case 'PLUS_ONE':
      return state + 1;  // +1 したり
    case 'MINUS_ONE':
      return state - 1;  // -1 したりする
  }
}

//Reducerから必要なStateを自動判定して、Storeを作る事ができる
let store = createStore(counter);

//Dispatch: 決めた内容に沿って実行いたします。
store.dispatch(plusOne()); //アクションを引数で指定する!

ここまでの簡単なまとめ

  • 「Store」:グローバルなstateの集まり
  • 「Action」:stateを変化させたい!という思念
  • 「Reducer」:「Action」をさらに具体化するもの
  • createStore(reducer)でStoreを生成できる
  • store.dispatch(action)でアクションを実行する

ファイルを分けて管理する

ここまでは同一ファイル内に全てを記述してきましたが、ここからはファイルを分けて管理していきます。

ファイル構成のおさらい(React Native + Redux)

  • ReactはViewの管理(見た目を作るコンポーネントなど)
  • ReduxはStateの管理(State管理のために「action」「reducer」「State」の3つが必要)

をそれぞれ行います。
したがって、ルート直下は以下のような構成とします。

[project root]
 `- [src]
     |-[actions]
     |-[reducers]
     |-[states]
     |-[views]
        |-[components]
        `-[containers]
           |-[App.js]

src/actions

もちろんアクションを記述していきます。

src/actions内に、jsファイルを作成しその中にアクションをまとめて記述します。
今回はカウンターを変化させるアクションを記述するのでsrc/actions/counter.jsとします。

他のファイルからインポートして使えるように、exportしておきます。

※先ほどindex.jsに記述したアクションと同じ内容です。↓

src/actions/counter.js
export const increment = () => (
  {type: 'INCREMENT'}
)

export const decrement = () => (
  {type: 'DECREMENT'}
)

src/reducers

ここにはリデューサーを記述していきます。

src/actions内に、jsファイルを作成しその中にアクションをまとめて記述します。
今回はカウンターを変化させるアクションを記述するのでsrc/actions/counter.jsとします。

※先ほどindex.jsに記述したアクションと同じ内容です。↓

src/actions/counter.js
const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case 'PLUS_ONE':
      return state + 1;

    case 'MINUS_ONE':
      return state - 1;

    default:
      return state;
  }
}
export default counterReducer;

「Reducer」を全て合体した「allReducer」を作る

先ほど、createStore(reducer)でStoreを生成できると言いました。

しかし、reducerは今後管理するStateが増えるにしたがってどんどん増えていきます。
たくさんのreducerを全てcreateStore(reducer)の引数に入れることはできませんので、

個々の「Reducer」を全て合体した「allReducer」を作り、1つの大きなReducerを作る必要があります。

src/reducers/index.jsを作る

reducersディレクトリの中にindex.jsを作成し、ここにallrReducer(個々のreducerの集合体)を作ります。

個々のreducerを合体させるためのメソッドとしてcombineReducers()がReduxには用意されています。

今回は、isLoggedReducerというreducerもある仮定で、先ほどのcounterReducerと合体させます。

src/reducers/index.js
import { combineReducers } from 'redux'; //合体メソッド
//
import counterReducer from './counter'; // さっきのカウンタ用Reducer
import isLoggedReducer from './isLogged'; // もしもう一個Reducerがあったら

const allReducer = combineReducers({
  counter: counterReducer, // state名:Reducer
  isLogged: isLoggedReducer // state名:Reducer
});

export default allReducer;

combineReducers()の引数には、{State名:Reducer}の形のオブジェクトを渡しています。

ここで、最終的なState名を設定できます。つまり、State名の部分はここで自由に命名できます。

src/store

このStoreに全てのStateを保管します。

src/store内にindex.jsを作成し、その中にstoreを記述します。

src/store/index.js
import { createStore } from 'redux'; // storeを作る関数
import allReducer from '../reducers'; //さっきの合体reducer

const store = createStore(allReducer);

export default store;

ここまでのまとめ

これで、ファイルの分割は完了しました!
基本的には何も特別なことはしていません。

新しく増えたのは、
combineReducers()を使って、複数のReducerを一つにまとめてから、createStoreに渡すだけです。

プロバイダーを設定する

作成した Storeを全てのコンポーネントから参照できるようにするために「プロバイダー」という仕組みを使います。

ここまでではまだ、Storeを作成しただけで、コンポーネントと繋がっていないので、Stateの変化に合わせてコンポーネントが再レンダリングされません。そのため「プロバイダー」使う必要があります。

src/views/App.js
import React from 'react';
import { Provider } from 'react-redux'; //インポートして使う
//
import store from '../../states' // さっきのStore
import ChildPage from '../components/ChildPage';

const App = () => {

    return (
        <Provider store={store}> propsにstoreを渡す
            <ChildPage /> ChildPage以下でStateが使える!
        </Provider>
    )
}
export default App;

コンポーネントを<Provider></Provider>で囲みます。
<Provider>のpropsにstoreを渡します。

囲まれたコンポーネント以下で、Store(State)を使用できるようになります。

Stateを更新する!

ではいよいよ、Stateを更新してみましょう。

<Provider></Provider>で囲んだコンポーネントにカウンターStateを更新するボタンを作ってみます。

src/views/component/ChildPage.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux'; 
import { Text, View, StyleSheet, Button} from 'react-native';
//
import { increment, decrement } from '../../actions';

const ChildPage = () => {
    const counter = useSelector(state => state.counter)
    const dispatch = useDispatch()
    return (
        <View style={styles.View}>
            <Text>カウンタ: {counter}</Text>
            <Button
                title="プラスわん!"
                onPress={() => dispatch(increment())} />
        </View>
    )
}
export default ChildPage;

useSelector, useDispatch

  • Stateを参照するには、useSelector()を使用します。
  • Stateを更新するためには、useDispatch()を使用します。

まとめ

Reduxは工程や覚える内容が多く複雑な印象ですが、実際にやってみると感覚を掴むのは早いと思います。