DEV Community

kaede
kaede

Posted on • Updated on

React Redux Tutorial Part 3 -- TypeScript の追加

何をやるのか

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/

npm i typescript @types/node @types/react @types/react-dom @types/jest
Enter fullscreen mode Exit fullscreen mode

@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 を組み込んで作成し直す。

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

npm i react-redux @reduxjs/toolkit
Enter fullscreen mode Exit fullscreen mode

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

import { configureStore } from '@reduxjs/toolkit'

export const store =configureStore({
  reducer: {},
})
export default store
Enter fullscreen mode Exit fullscreen mode

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

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

  <Provider store={store}>
    <App />
  </Provider>
Enter fullscreen mode Exit fullscreen mode

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

Image description

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 は後ほど作る。

const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
})
export default store
Enter fullscreen mode Exit fullscreen mode

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

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
Enter fullscreen mode Exit fullscreen mode

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


hooks を作る

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

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

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

import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'
Enter fullscreen mode Exit fullscreen mode

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

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

export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
Enter fullscreen mode Exit fullscreen mode

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

これらを hooks から export

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


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

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

interface CounterState {
  value: number
}
const initialState: CounterState = {
  value: 0,
}
Enter fullscreen mode Exit fullscreen mode

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

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
Enter fullscreen mode Exit fullscreen mode

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


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

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

  reducers: {
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload
    },
  },
Enter fullscreen mode Exit fullscreen mode

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

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
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

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

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

export default counterSlice.reducer
Enter fullscreen mode Exit fullscreen mode

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


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

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

  const count = useAppSelector((state) => state.counter.value)
  const dispatch = useAppDispatch()
Enter fullscreen mode Exit fullscreen mode

useAppSelector, useAppDispatch を import して

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

<button
  aria-label="Increment value"
  onClick={() => dispatch(increment())}
>
  Increment
</button>
<span>{count}</span>
<button
  aria-label="Decrement value"
  onClick={() => dispatch(decrement())}
>
  Decrement
</button>
Enter fullscreen mode Exit fullscreen mode

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


動作確認

Image description

動作確認ができた。


まとめ

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 アプリを作るチュートリアルに進む。

Top comments (0)