Reactを始めてすぐにReduxを理解しようとすると頭が事故るので、まずは感覚的につかむために自分なりにやりやすかったファイル構成と書き方をまとめた。

目次

State

初期値とその型を作成。

// store/user/state.ts

export interface UserState {
  id: number | null
  name: string
}

export const initialState: UserState = {
  id: null,
  name: '',
}

Action

イベントのハンドラーとなる関数とその種別を定義。
ハンドラーに渡す引数は違いを付けるために型を変えているが、引数もしくは返り値のpayloadの部分はstateの型に合わせておく方が後々楽に感じた。

// store/user/action.ts

import { UserState } from './state'

export const ACTION_TYPE = {
  CREATE_USER: 'CREATE_USER',
  UPDATE_USER: 'UPDATE_USER',
  DELETE_USER: 'DELETE_USER',
} as const

export type ActionType = typeof ACTION_TYPE[keyof typeof ACTION_TYPE]

export const createUser = (name: string) => ({
  type: ACTION_TYPE.CHANGE_TAB,
  payload: name
})

export const updateUser = (user: UserState) => ({
  type: ACTION_TYPE.UPDATE_USER,
  payload: user
})

export const deleteUser = (id: number) => ({
  type: ACTION_TYPE.DELETE_USER,
  payload: id
})

export type Action = ReturnType<typeof createUser | typeof updateUser | typeof deleteUser>

Reducer

上記で設定した種別にあわせて返り値を振り分けていく。
ここでdefaultがないとエラーか警告が出たはずなので注意が必要。

// store/user/reducer.ts

import { initialState } from './state'
import { Action, ACTION_TYPE } from './action'

export const userReducer = (state = initialState, action: Action) => {
  switch (action.type) {
    case ACTION_TYPE.CREATE_USER:
      return { ...state, name: action.payload }
    case ACTION_TYPE.UPDATE_USER:
      return { ...state, ...action.payload }
    case ACTION_TYPE.DELETE_USER:
      return { ...state, id: action.payload }
    default:
      return state
  }
}

フォルダでimportできるように

インポートする際にストア名だけでできるようにしておく。

// store/user/index.ts

export * from './state'
export * from './action'
export * from './reducer'

ストア生成

今回はuserストアしかないが、複数のストアを扱う時は以下のような形にすると結合できる。
あまりないと思うが一つでよい場合はcombineReducersは使わずにcreateStoreの引数にそのままreducerを渡す。

// store/index.ts

import { combineReducers, createStore } from 'redux'
import { userReducer, UserState } from '@/store/user'

export interface State {
  user: UserState
}

const reducers = combineReducers({  
  user: userReducer,
})

export const store = createStore(reducers)

実用例(SelectorとDispatcher)

TextButtonのインポートは割愛。
useSelectorのジェネリクス指定はルートステートと、返り値の型を指定すると補完も効くようになる。
dispatchの引数はactionで定義したハンドラーを呼び出す。
この辺りは呼び出し側がややこしくなる可能性もあるので、useDispatchをラップして各ハンドラーを集めたフックを別に作った方が見やすくなるかも。(だがしない)

import { useSelector, useDispatch } from 'react-redux'
import { State } from '@/store'
import { createUser, updateUser, deleteUser } from '@/store/user'

export const Component = () => {
  const user = useSelector<State, State['user']>(state => state.user)
  const dispatch = useDispatch()

  return (
    <>
      <Text>ID: { user.id }</Text>
      <Text>名前: { user.name }</Text>
      <Button onClick={ () => dispatch(createUser('作成名')) }>
        作成
      </Button>
      <Button onClick={ () => dispatch(updateUser({ id: 1, name: '更新名' })) }>
        更新
      </Button>
      <Button onClick={ () => dispatch(deleteUser(1)) }>
        削除
      </Button>
    </>
  )
}

やりやすいファイル構成を自分なりにとは書いたが、ぶっちゃけ誰がやっても似た形になりそうなことをこの文を書いている最中に気づいたのは内緒。

プロフィール画像

ふじわら

よくわからないもので戯れてたら自分のことすらよくわからない人間になってしまいました。

ひっそりYouTubeしてます。