Redux-toolkitを使ったStoreをtypescriptで作ってみる
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