🙆

next.js + typescript で Redux Toolkit を使う最小サンプル

2021/09/23に公開

公式のサンプルに使い方が丁寧に書かれているが、
きれいにファイル分割されていて、ひさしぶりに見ると逆に分かりづらかったので、
必要な部分だけ最小の構成でReduxToolkitを使うサンプルをまとめておく。

注意

プロジェクト作成後、サーバを起動したままで進めて行くと、
Providerタグを追加後、useDispatch() を呼び出すところで、
please ensure the component is wrapped in a <Provider>
のエラーになることがあるが、サーバを起動しなおせば正しく動く(はず)。

next.js プロジェクト作成

npx create-next-app redux_toolkit_sample --ts

pages/index.tsx をシンプルな表示にする

pages/index.tsx はカウントとボタンだけを表示するようにしておく。

pages/index.tsx
import { NextPage } from "next"

const Home: NextPage = () => {
  return (
    <div>
      <p>count</p>
      <button>click</button>
    </div>
  );
};

export default Home;

pages/_app.tsx の編集

_app.tsx に必要な処理を追加していく。
公式サンプルではファイルを分けて作成しているが、ここではファイル数も少なくしたいので、_app.tsx に書けるものはここで書いてしまう。
(このセクションの最後に、最終的なコードがあります。)

※分かりやすさ重視で1ファイルにまとめているので、業務で作る場合は公式のサンプルにあるように、features単位でsliceをまとめていくほうが良い。

reduxで管理するstateの作成

カウント数を管理するCounterState型を作成する。
また、初期値を設定する変数を定義する。

pages/_app.tsx
// typeの定義
export type CounterState = {
  value: number;
};
// 初期値の定義
const initialState: CounterState = { value: 0 };

sliceの作成

カウントを増加させるincrementアクションを作成する。

pages/_app.tsx
import { createSlice } from "@reduxjs/toolkit";

export const counterSlice = createSlice({
  // 名前
  name: "counter",
  // 初期値
  initialState,
  // reducer
  reducers: {
    increment(state) {
      state.value++;
    },
  },
});

storeの作成

reducerを指定してstoreを作成

pages/_app.tsx
import { configureStore, createSlice } from "@reduxjs/toolkit";

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

reduxを使うのでProviderで囲む

子コンポーネントをProviderで囲み、storeを指定しておく。

pages/_app.tsx
import { Provider } from "react-redux";

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <Provider store={store}>
      <Component {...pageProps} />
    </Provider>
  );
}

ここまでのコードをまとめたもの

pages/_app.tsx
import React from "react";
import type { AppProps } from "next/app";
import { Provider } from "react-redux";
import { configureStore, createSlice } from "@reduxjs/toolkit";

export type CounterState = {
  value: number;
};

const initialState: CounterState = { value: 0 };

export const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    increment(state) {
      state.value++;
    },
  },
});

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

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <Provider store={store}>
      <Component {...pageProps} />
    </Provider>
  );
}
export default MyApp;

pages/index.tsx の編集

実際に表示するページを作成する。

useDispath

アクションを起動する dispatch 関数を store から取得するため、useDispatch で取得しておく。

pages/index.tsx
import { useDispatch } from "react-redux";

const dispatch = useDispatch();

useSelector

storeからstateを持ってくるために useSelectorフックを使う。
今のところ、store には CounterState しか無いので、
store.getState().counter としても CounterState は取れるが、
フックを使って取るようにしておく。

pages/index.tsx
import { useSelector } from "react-redux";

const selector = useSelector((state: CounterState) => state);

カウンターを表示する

selectorからCounterStateを参照できるので、ここからカウンターの値を取得して表示する。

pages/index.tsx
  return (
    <div>
      <p>{selector.value}</p>
      <button>Click</button>
    </div>
  );

increment アクションを取得する

sliceのactionsから、必要なアクション increment だけ取得する。

pages/index.tsx
const { increment } = counterSlice.actions;

ボタンをクリックしたら increment アクションを実行する

pages/index.tsx
<button
  onClick={() => {
    dispatch(increment());
  }}
>
  Click
</button>

ここまでのコードをまとめたもの

pages/index.tsx
import { useDispatch, useSelector } from "react-redux";
import { counterSlice, CounterState, store } from "./_app";
import { NextPage } from "next";

const Home: NextPage = () => {
  const dispatch = useDispatch();
  const selector = useSelector((state: CounterState) => state);
  const { increment } = counterSlice.actions;

  return (
    <div>
      <p>{selector.value}</p>
      <button
        onClick={() => {
          dispatch(increment());
        }}
      >
        Click
      </button>
    </div>
  );
};

export default Home;

storeに複数のstateがある場合

複数のstateを取りまとめるRootStateを作成し、
storeには、それぞれのstateを処理するreducerを登録。
useSelectorで取得したいstateを選択すれば良い。

export type RootState = {
  counter: CounterState;
  address: AddressState;
};

const counterInitialState: CounterState = {
  value: 0,
};

const addressInitialState: AddressState = {
  zipCode: "000-0000",
};

export const counterSlice = createSlice({
  name: "counter",
  initialState: counterInitialState,
  reducers: {
    increment(state) {
      state.value++;
    },
  },
});

export const addressSlice = createSlice({
  name: "address",
  initialState: addressInitialState,
  reducers: {},
});

export const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
    address: addressSlice.reducer,
  },
});

useSelectorで使うときは、

  const counterSelector = useSelector((state: RootState) => state.counter);
  const addressSelector = useSelector((state: RootState) => state.address);

Discussion