React Redux Tutorial Part 3 -- TypeScript の追加
Tags: redux, typescript
Published: 2022-05-01 03:30:33


## 何をやるのか

https://react-redux.js.org/tutorials/typescript-quick-start

React Redux Tutorial

カウンターアプリに TS を導入し、
呼び出し元で意識することを少なくするために

* グローバルステートを取ってくる useAppSelector
* グローバルステートの変更メソッドを呼び出す useAppDispatch

これらを hooks に定義する。

---

## 前回作ったプロジェクトに TS を追加しようとして失敗

https://makeanapplike.com/yarn-create-react-app-typescript-redux-npm/

```ts
npm i typescript @types/node @types/react @types/react-dom @types/jest
```

`@types` の node, react, react-dom, jest, のライブラリを追加

index.js を index.tsx に変更

サーバーを再起動

これだけで TS が動かせるようになるらしい。
実際に動かしたらエラーは出なかった。

しかし、その後 App.js, store.js, Counter.js を ts に書き直すと

> Can't resolve './app/store' in '/Users/kaede0902/code/redux/src'

このように読み込めなくなってしまった。

https://iwb.jp/webpack-config-js-module-not-found-error-cant-resolve/

この記事を読むと、Webpack を導入しないと index 以外の TS は読み込めないらしい。

---

## CRA with TS で作って前回の記事のところまでアプリを作り直す

ライブラリの追加だけでは TS ファイルを読み込めなかったので、
CRA の時点で TS を組み込んで作成し直す。

```php
npx create-react-app ts-redux --template typescript

npm i react-redux @reduxjs/toolkit
```

CRA を TS で作って、
react-redux @reduxjs/toolkit のライブラリを入れる

```ts
import { configureStore } from '@reduxjs/toolkit'

export const store =configureStore({
  reducer: {},
})
export default store
```

これで app/store に store ファイルを TS で作って

```ts
import store from './app/store'
import { Provider } from 'react-redux'

  <Provider store={store}>
    <App />
  </Provider>
```

index.tsx で読み込んで動作確認すると

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f9w2jswzs22iiq3d0pkg.png)

TS で書いたストアファイルがちゃんと読み込まれているのを確認できた。

https://dev.to/kaede_io/react-redux-part-1-react-redux-nodao-ru-5o1

そして、前回の記事通りに Counter アプリを ts や tsx の拡張子で作成する


---


## app/store のストアファイルでの configureStore の export を止め、 RootState と AppDispatch の型を定義して export する

store という変数で reducer を　configureStore でまとめる。

countReducer は後ほど作る。

```ts
const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
})
export default store
```

このように、単に reducer をラップして export していた
configureStore を変更する。そして default で export
これで {} をつけなくても import できる

```ts
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
```

そしてその変数 store から 
getState するときの type を RootState
dispatch するときの type を AppDispatch
として export する。

---

## hooks を作る

ストアファイルに作った RootState と AppDispatch は
そのまま描画コンポーネントで使う訳ではない。

Custom Hooks を定義し、ここでラップされる。

src/app/ に hooks.ts を作成し

```ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'
```

react-redux の npm ライブラリから
普段の　useDispatch, useSelector の他に
TypedUseSelectorHook もインポート

さらに先ほど作った　store ファイルから RootState と AppDispatch をインポート

```ts
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
```

AppDispatch の型を使って useDispach する useAppDispach
TypedUseSelectorHook と RootState の型でチェックして useSelector を入れる？ useAppSelector

これらを hooks から export

そして今まで使っていた useDispatch と useSelector をこれらに変更する。

---

## features/counter/CounterSlice でグローバルステート counter 初期値を数値型で定義し直す

次は Slice ファイルに　TS を導入する。

```ts
interface CounterState {
  value: number
}
const initialState: CounterState = {
  value: 0,
}
```

値が数値型になる interafce の CounterState を定義
CounterState を使って 初期値 0 で initialState を定義

```ts
export const counterSlice = createSlice({
  name: 'counter',
  initialState,
```

その initialState を counter を作る時に初期値として使った。

---

## CounterSlice で reducers の incrementByAmount がちゃんと action.payload の形だけ受け取れるようにする

```ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

  reducers: {
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload
    },
  },
```

action.payload に余計なものが入らないようにした。

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

counterSlice コンポーネントの全体はこうなる。

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

export default counterSlice.reducer
```

actions や reducers を export するのも忘れずに。

---

## Counter コンポーネントで hooks の useAppSelector と useAppDispatch を呼び出す

```ts
import { useAppSelector, useAppDispatch } from '../../app/hooks'

  const count = useAppSelector((state) => state.counter.value)
  const dispatch = useAppDispatch()
```

useAppSelector, useAppDispatch を import して

count, dispatch としてコンポーネント内部で定義して

```ts
<button
  aria-label="Increment value"
  onClick={() => dispatch(increment())}
>
  Increment
</button>
<span>{count}</span>
<button
  aria-label="Decrement value"
  onClick={() => dispatch(decrement())}
>
  Decrement
</button>
```

ボタンに組み込んで使う。

---

## 動作確認

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8brd5i7tl0c29ranbfka.png)

動作確認ができた。 

---

## まとめ

JS で書いていた Redux の Counter アプリを
TS でモダンに書き換えるためには

モジュールとして export していた store を変数にして
それを使って RootState と AppDispatch の型を作成

hooks というファイルでそれらと reduxtoolkit の
TypedUseSelectorHook, useDispatch, useSelector
これらを合わせて
useAppSelector, useAppDispatch
というものにまとめて

counterSlice の slice ファイルで、初期値に型をつけ
payload を使う reducer にも型をつけ

Counter コンポーネントで
hooks の useAppSelector, useAppDispatchを使い、
useAppSelector で グローバルステートの counter を呼び出し
useAppDispatch で counterSlice の decrement, increment を呼び出し
それぞれ表示部分とボタンの関数部分に組み込む。

## 今後

https://react-redux.js.org/tutorials/connect

connect API を使って
Todo アプリを作るチュートリアルに進む。