【React】 Reduxことはじめ

React Reduxは、Reactにおけるステートを管理するためのものです。これを用いることで、コンポーネント間のデータのやり取りをスムーズに行うことができます。

Reduxの利点

Reduxを用いることで、ステートの管理を容易に行うことができるようになります。今まではステートをpropで渡しました。更新する際は親にイベントを伝えて更新を行いました。

この時、親子関係にあるコンポーネントは良いですが、そうではないコンポーネント間でのステートのやり取りは困難です。Reduxでは、そういったやり取りも容易に行なえます。

Propは親子関係でないと渡せないが、Reduxは制限がない

また、Reactでは、非同期な更新ができませんでした。

const updateHoge = useCallback(async () => {
    // ...

    // NG 非同期な関数のため、ステートの更新ができない
    setHoge(100);
});

Reduxでは、非同期にステートを更新することができます。 (執筆中)

Reduxの導入

npmでreduxを導入する場合は、react-redux @reduxjs/toolkit @types/react-reduxの3つを導入すると良いです。

npm i react-redux @reduxjs/toolkit
npm i -D @types/react-redux

もしcreate-react-appで新規で開発を行う場合は、create-react-appにあるreduxのテンプレートを用いるのも良いです。

npm i -D create-react-app

# myappは適当なアプリ名で
npx create-react-app myapp --template redux

Reduxのステートの更新の流れ

Reactにおいては、ViewからActionsを起こし、それがStateを更新し、Viewに反映されるという一方向に処理が流れます。useStateを使用した際はこのように処理が流れ、シンプルな値の扱いができます。しかし、コンポーネント間で複雑なデータのやり取りが絡むと途端に管理が難しくなります。

Reduxでは、ステートを (内部では) 1箇所で管理し、どのように更新するかを定義し、それを利用してステートが更新されます。更新する内容を定義した関数は我々が直接呼び出すわけではなく、更新時にReduxが勝手に呼び出します。

ところで、Reduxの公式ドキュメントには、データの流れを示した次のような図があります。

Reduxのデータフロー
https://redux.js.org/tutorials/essentials/part-1-overview-concepts#why-should-i-use-redux https://github.com/reduxjs/redux/blob/master/LICENSE.md

この流れを言葉で表すと、まず初期値を定義した上で、

  1. ボタンなどがクリックされる
  2. 普通にイベントが発火して、dispatchする (このときに、更新に使う関数を指定する)
  3. Reduxが、指定された関数を使ってステートを更新
  4. ビューに反映される

という流れです。

Reduxを使う

早速Reduxを使ってみましょう。今回はcreate-react-appreduxテンプレートで作成されるものを参考に用意していきます。

Reduxを利用できるようにする

まずは、そのReactのアプリで、Reduxが使える範囲を指定します。アプリ全体でも問題ありませんが、パフォーマンス等の都合で分けたい場合は分けると良いです。

// ...

import { Provider } from 'react-redux';

// 後で追加します
import { store } from './app/store';

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    {/* ここで指定したstoreが使用できる */}
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>  
);

sliceの準備

sliceを用いると、ステートの初期値、名前、Reducer、Actionの4つを一括で管理できます。Reducerはステートを更新するためのもので、Actionは更新の際に使うデータなどが入っているものです。ReducerはActionを受け取って更新します。(受け取る必要がない場合は省略できます。)

まずは箱を作って、exportしておきましょう。

features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

export const counterSlice = createSlice({
    // ここに定義を書く
});

名前決め

まずはnameキーを使って、そのsliceの名前を決めます。

export const counterSlice = createSlice({
    name: 'counter',
});

ステートの初期値

ステートの初期値は、initialStateキーに入れます。連想配列内に直接書いても良いですが、可読性を上げるため、外出しする場合も多いです。

// ステートの初期状態
const initialState = {
    value: 0,
};

export const counterSlice = createSlice({
    name: 'counter',
    initialState,
});

Reducerを定義

Reducerは、状態を更新するためのものです。ここにどのように更新するかを定義します。今回は、1増やす、1減らす、指定数増やすの3つを用意します。

export const counterSlice = createSlice({
    name: 'counter',
    initialState,
    reducers: {
        increment: (state) => {
            state.value += 1;
        },
        decrement: (state) => {
            state.value -= 1;
        },
        incrementByAmount: (state, action) => {
            state.value += action.payload;
        },
    },
});

各Reducerの第1引数は、これが呼ばれた時点でのステートです。これを直接書き換えて値を更新します。

第2引数には、dispatchする際に関数に入れた値がaction.payload内にそのまま入ってきます。必要なければ省略して問題ありません。

Reducerをexport

作ったreducerを利用できるように、exportしましょう。作ったreducerは、slice.actionsで取れます。

export const { increment, decrement, incrementByAmount } = counterSlice.actions;

state取得用の関数をexport

stateを取得するには、Reducer本体のstateから取り出す必要があります。そのための実装を用意します。

取り出すには、Reduxが呼び出すための関数を用意し、その第1引数にRootState型の値を受け取ります。RootStateには、Reduxに入れたすべてのstateが入っています。

この中から適切なstateを取り出すには、createSliceで指定したnameを用いて値を取り出します。

export const selectCount = (state) => state.counter.value;

作った関数は、useSelectorで値を取得する際に使用します。

これで値を取り出すための関数ができました。

reducerをexport

次に、ReduxがどのようなReducerがあるかを認識するために、reducerをexportしておきます。

export default counterSlice.reducer;

counterSlice全体

ここまでのコードを並べると、次のようになります。

import { createSlice } from '@reduxjs/toolkit';

const initialState = {
    value: 0,
};

export const counterSlice = createSlice({
    name: 'counter',
    initialState,
    reducers: {
        increment: (state) => {
            state.value += 1;
        },
        decrement: (state) => {
            state.value -= 1;
        },
        incrementByAmount: (state, action) => {
            state.value += action.payload;
        },
    },
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;

export const selectCount = (state) => state.counter.value;

export default counterSlice.reducer;

storeを設定

storeは、プログラミングでは格納するという意味でしばしば使用されます。例えば、データをストレージにストアする、というような言い方をします。

ここでは、createSliceで作成したreducerconfigureStoreで登録することで、Reduxにどんなreducerがあるかを伝えます。

app/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';

export const store = configureStore({
    reducer: {
        counter: counterReducer,
    },
});

上の例では今回作成したcounterのReducerだけですが、複数ある場合はすべてここに登録していきます。

さて。これでRedux側のデータの取り扱い周りの作成は完了しました。次は実際に利用するところを見ていきます。

値を取得

Reduxで管理している値を取得するには、useSelector上でexportしたselectorを用います。

import { useSelector } from 'react-redux';
import { selectCount } from './features/counter/counterSlice';

export default function App() {
    // selectCount() ではない
    const count = useSelector(selectCount);

    return (
        <>
        <p>{ count }</p>
        </>
    );
}

値を更新

値を更新する際は、useDispatchを用いてディスパッチします。引数には、値更新で動かしたいアクションを指定します。

import { useSelector, useDispatch } from 'react-redux';
import { increment, selectCount } from './features/counter/counterSlice';

export default function App() {
    const count = useSelector(selectCount);
    // 必ず忘れないようにする!!
    const dispatch = useDispatch();

    const handleIncrement = () => {
        dispatch(increment());
    }

    return (
        <>
        <p>{ count }</p>
        <button onClick={handleIncrement}>+1</button>
        </>
    );
}

dispatch(increment());する処理をuseCallback内にはあまり入れません。その関数を子のコンポーネントに渡す必要がない場合が多いためです。

もし子コンポーネントに関数を渡すのであればuseCallbackしますが、そもそもReduxにある値はどこからでも更新できるので、関数ごと渡す必要性が低いです。

実際に動かしてボタンを押すと、1ずつ増えていくことがわかります。

actionを指定

reducerの実装時に、

incrementByAmount: (state, action) => {
    state.value += action.payload;
},

を作成しました。ここのactionという引数に値を入れるには、dispatchするときにアクションの引数に伝えたい値を入れます。

import { increment, incrementByAmount, selectCount } from './features/counter/counterSlice';

export default function App() {
    const count = useSelector(selectCount);
    const dispatch = useDispatch();

    const handleIncrementByAmount = () => {
        // 関数の第1引数に入れる
        dispatch(incrementByAmount(5));
    }

    return (
        <>
        <p>{ count }</p>
        <button onClick={handleIncrementByAmount}>+5</button>
        </>
    );
}

このようにして値を入れることで、reducerに値を伝えることができます。action引数では、action.payloadとすると、入れた値を取得できます。

複数の値を入れたい場合は、連想配列を用いると良いです。

dispatch(hoge({
    abc: 100,
    foo: "bar",
}));
incrementByAmount: (state, action) => {
    state.value += action.payload.abc;
    state.baz = `foo: ${action.payload.bar}`;
},

まとめ

Reduxは、Reactでステートを管理するためのシステムです。Reduxを用いることで1箇所でステート管理できるので、どのコンポーネントからも同じ値にアクセスし、更新することができます。

まずは基本的な使い方を覚えて、その後より良い設計や非同期の更新などを学んでいきましょう。

react redux basic thumb

役に立ったらシェアしよう!