🧐

Redux-toolkitを使ったStoreをtypescriptで作ってみる

2022/01/09に公開

Redux-toolkitを使ったStoreを作ってみる

何度やっても覚えられないので、自分用に導入手順を備忘録的に書き起こしておこうと思います。

参考にした記事

TITLE LINK
Redux入門者向け初めてのRedux ToolkitとRedux Thunkの非同期処理 https://reffect.co.jp/react/redux-toolkit
React + Typescript プロジェクトに Redux Toolkit を導入したので使い方をざっくりとまとめてみる https://dev.classmethod.jp/articles/react-typescript-redux-toolkit/
TypeScriptでReactをやるときは、小さいアプリでもReduxを最初から使ってもいいかもねというお話 https://future-architect.github.io/articles/20200501/

プロジェクト作成

既存プロジェクトに導入する場合はスキップ。

$ yarn create react-app ./redux-sample --template=typescript
$ cd ./redux-sample

linterなどの設定

いらない人や自身の設定がある人はスキップ。

$ yarn add -E -D eslint prettier @typescript-eslint/{eslint-plugin,parser} \
   eslint-config-prettier \
   eslint-plugin-{import,jsx-a11y,prettier,react,react-hooks}
  • .editorconfig

    root = true
    
    [*]
    charset = utf-8
    end_of_line = lf
    indent_style = space
    indent_size = 2
    insert_final_newline = true
    trim_trailing_whitespace = true
    
    [*.md]
    trim_trailing_whitespace = false
    
  • .eslintrc.js

    module.exports = {
      parser: '@typescript-eslint/parser',
      env: {
        browser: true,
        es2021: true,
      },
      extends: ['react-app', 'prettier'],
      parserOptions: {
        ecmaFeatures: {
          jsx: true,
        },
        ecmaVersion: 12,
        sourceType: 'module',
      },
      plugins: ['prettier'],
      rules: {
        'prettier/prettier': 'error',
      },
      overrides: [
        {
          files: ['**/*.stories.*'],
          rules: {
            'import/no-anonymous-default-export': 'off',
          },
        },
      ],
    };
    
  • .prettierrc.json

    {
      "printWidth": 150,
      "useTabs": false,
      "semi": true,
      "singleQuote": true
    }
    
  • package.json

    scriptsオブジェクトのeject下あたりに追記

    "lint": "npx eslint 'src/**/*.ts{,x}' --fix",
    "prebuild": "npx eslint 'src/**/*.ts{,x}' --fix",
    

redux関連パッケージのインストール

今回は素のReduxではなくtoolkitを使うのふたつインストール

# yarn add @reduxjs/toolkit react-redux

実装

React Hookで仮実装

まず簡易的にhookで仮実装してからreduxに置き換えていきたいと思います。

実装

hookのsampleとしてよく扱われる四則演算を画面上のボタン押下によって実行し描画する実装を行っています。

  • src/app.tsx

    import './App.css';
    import { FC, useState } from 'react';
    
    const App: FC = () => {
      const [count, setCount] = useState<number>(0);
    
      const addition = (num: number) => {
        if (Number.isNaN(num)) return;
        setCount(count + num);
      };
      const subtraction = (num: number) => {
        if (Number.isNaN(num)) return;
        setCount(count - num);
      };
    
      return (
        <div className="App">
          <h1>Count: {count}</h1>
          <button onClick={() => addition(1)}>Up</button>
          <button onClick={() => subtraction(1)}>Down</button>
        </div>
      );
    };
    
    export default App;
    

Storeの実装

Storeの役割

プロジェクト内で実装している各コンポーネントからアクセスが可能なもの。

実態を管理するそれぞれのsliceをまとめるためのものです。

実装

  • src/store.ts

    import { configureStore } from '@reduxjs/toolkit';
    
    export const store = configureStore({
      reducer: {},
    });
    
    export type AppDispatch = typeof store.dispatch;
    

Sliceの実装

Sliceの役割

Sliceでは、hookで先ほど実装したような変数の格納、初期化、加算・減算など変数に対する処理を行うような役割を果たします。

このSliceは 管理したい値ごとにファイルを作成する 必要があります。

今回は count という変数を扱うsliceなので counterSlice.ts という名称でファイルを作成しましょう。

  • src/counterSlice.ts

    import { createSlice } from '@reduxjs/toolkit';
    
    export const counterSlice = createSlice({
      name: 'counter',
      initialState: {
        count: 0,
      },
      reducers: {
        additional: (state, action) => {
          if (Number.isNaN(action.payload)) return;
          state.count += action.payload;
        },
        subtraction: (state, action) => {
          if (Number.isNaN(action.payload)) return;
          state.count -= action.payload;
        },
      },
    });
    
    export const { additional, subtraction } = counterSlice.actions;
    export default counterSlice.reducer;
    

先ほど src/App.tsx で実装した additional , subtract をreducerの中で実装しています。

そのため、外部からこのreducerを呼び出せるように関数そのものをexportしています。

StoreにSliceを追加する

Storeに追加実装

先ほど作成した変数など値をコンポーネントから参照できるよう、counterReducerをstoreに追加します。

また、selectorで外部からreducerというかsliceの値を取得する際、読み取り側でreact-reduxからuseSelectorをimportして使ってしまうと、stateに対して型補完が効かなくなります。

そのため、最下行あたりでexportを追加しています。

具体的には、まずRootState内でstore内のstateを型で取得し、返却するようにしています。

続いて react-redux から useSelector を別名(rawUseSelector)でimportし、useSelectorの型である TypedUseSelectorHook にRootStateを当てて返しています。

こうすることで補完されます。

  • src/store.ts

    import { configureStore } from '@reduxjs/toolkit';
    import { useSelector as rawUseSelector, TypedUseSelectorHook } from 'react-redux';
    import counterReducer from './counterSlice';
    
    export const store = configureStore({
      reducer: {
        counter: counterReducer,
      },
    });
    
    export type AppDispatch = typeof store.dispatch;
    export type RootState = ReturnType<typeof store.getState>;
    export const useSelector: TypedUseSelectorHook<RootState> = rawUseSelector;
    

Selectorを実装

App.tsxからstoreを参照する

selectorを利用することでstoreの中の値を取得することが可能になります。

  • src/App.tsx

    import './App.css';
    import { FC, useState } from 'react';
    import { useSelector } from './store';
    
    const App: FC = () => {
      const count = useSelector((state) => state.counter.count);
      // const [count, setCount] = useState<number>(0);
      // const addition = (num: number) => {
      //   if (Number.isNaN(num)) return;
      //   setCount(count + num);
      // };
      // const subtraction = (num: number) => {
      //   if (Number.isNaN(num)) return;
      //   setCount(count - num);
      // };
    
      return (
        <div className="App">
          <h1>Count: {count}</h1>
          {/* <button onClick={() => addition(1)}>Up</button>
          <button onClick={() => subtraction(1)}>Down</button> */}
        </div>
      );
    };
    
    export default App;
    

※ 先述した実装は差異を見せるために一旦コメントアウトした状態にして残しています。

Dispatchを実装

storeの中の値を変更するreducerを実行する

reducerで実装した additional, subtraction を実行するためには、Dispatchを通す必要があります。

Dispatchの実装

  • src/App.tsx

    import './App.css';
    import { FC, useState } from 'react';
    import { useSelector } from './store';
    import { useDispatch } from 'react-redux';
    import { additional, subtraction } from './counterSlice';
    
    const App: FC = () => {
      const count = useSelector((state) => state.counter.count);
      const dispatch = useDispatch();
      // const [count, setCount] = useState<number>(0);
      // const addition = (num: number) => {
      //   if (Number.isNaN(num)) return;
      //   setCount(count + num);
      // };
      // const subtraction = (num: number) => {
      //   if (Number.isNaN(num)) return;
      //   setCount(count - num);
      // };
    
      return (
        <div className="App">
          <h1>Count: {count}</h1>
          {/* <button onClick={() => addition(1)}>Up</button>
          <button onClick={() => subtraction(1)}>Down</button> */}
          <button onClick={() => dispatch(additional(1))}>Up</button>
          <button onClick={() => dispatch(subtraction(1))}>Down</button>
        </div>
      );
    };
    
    export default App;
    

    最後に

    非同期処理のAsyncThunkなど書こうと思いましたがまた後でやろうと思います
    書きました -> Redux-toolkitを使ったStoreをtypescriptで作ってみる

    更新履歴

    2022/01/10: src/App.tsx内のimportに関する記述が一部ローカル用の記述になっていたため修正

Discussion