Chapter 08

第6章 Reduxでstate管理 後編 -meta state・app state・カスタムhooks-

てべすてん
てべすてん
2022.03.11に更新

各stateの実装(続き)

前章に引き続きmeta,app stateも実装していきましょう。

meta state

これらをプロジェクトルート/src/redux/meta/内に

  1. types.ts
  2. reducers.ts
  3. selectors.ts
  4. actions.ts

を追加してそれぞれ以下のように記述していきます。(それぞれのファイルの内容は1つ前の章を参考にしてください)

types.ts
プロジェクトルート/src/redux/meta/types.ts
//特に定義するものがないが後で追加する可能性があるので念のため作成しておく
export {} ;
reducers.ts
プロジェクトルート/src/redux/meta/reducers.ts
import { ItemId } from "redux/items/types";
import { reducerWithInitialState } from "typescript-fsa-reducers";
import * as actions from "./actions" ;

export const init = {
    title:"タイトル未設定",
    flows:[] as ItemId[] ,
};

export const meta = reducerWithInitialState(init) 
    .case( actions.setTitle , (state,payload)=>{
        if(state.title === payload.title) return state ;
        const newState = {
            ...state,
        } ;
        newState.title = payload.title ;
        return newState ;
    } )
    .case( actions.addFlow , (state,payload)=>{
        const newState = {...state} ;
        newState.flows = [
            ...newState.flows,
            payload.flowId
        ] ;
        return newState ;
    } )
    .case( actions.removeFlow , (state,payload)=>{
        if(!state.flows.includes(payload.flowId))return state ;
        const newState = {...state} ;
        newState.flows = newState.flows.filter(flowId=>flowId !== payload.flowId) ;
        return newState ;
    })
    ;
selectors.ts
プロジェクトルート/src/redux/meta/selectors.ts
import { StoreState } from "redux/store";

export const getTitle = ()=>{
    return (state:StoreState)=>state.meta.title ;
} ;
export const getFlows = ()=>{
    return (state:StoreState)=>state.meta.flows ;
} ;
actions.ts
プロジェクトルート/src/redux/meta/actions.ts
import { ItemId } from "redux/items/types";
import actionCreatorFactory from "typescript-fsa" ;

const actionCreator = actionCreatorFactory() ;

//titleに関するActionCreator
export const setTitle = actionCreator<{title:string}>("meta/setTitle") ;

//flowに関するActionCreator
export const addFlow = actionCreator<{flowId:ItemId}>("meta/flow/add");
export const removeFlow = actionCreator<{flowId:ItemId}>("meta/flow/remove");

app state

app stateも同様にプロジェクトルート/src/redux/app/内に

  1. types.ts
  2. reducers.ts
  3. selectors.ts
  4. actions.ts

を追加して記述していきます。

types.ts
プロジェクトルート/src/redux/app/types.ts

//特に定義するものがないが後で追加する可能性があるので念のため作成しておく
export {} ;

reducers.ts
プロジェクトルート/src/redux/app/reducers.ts
import { reducerWithInitialState } from "typescript-fsa-reducers";

export const init = {
} ;

export const app = reducerWithInitialState(init) ;

selectors.ts
プロジェクトルート/src/redux/app/selectors.ts

//特に定義するものがないが後で追加する可能性があるので念のため作成しておく
export {} ;

actions.ts
プロジェクトルート/src/redux/app/actions.ts

//特に定義するものがないが後で追加する可能性があるので念のため作成しておく
export {} ;

最後にstore.tsにmetaとappを追加しておしまいです。

プロジェクトルート/scr/redux/store.ts
  import { combineReducers, createStore } from "redux" ;
  import { items } from "redux/items/reducers" ;
+ import { meta } from "./meta/reducers";
+ import { app } from "./app/reducers";

  export const rootReducer = combineReducers({
      items,
+     meta,
+     app,
  }) ;
  export const store = createStore(rootReducer) ;
  export type StoreState = ReturnType<typeof store.getState> ;

これでようやくReduxのstate定義が一通り完成しました。

カスタムhooksの作成

コンポーネント内でstoreのstateを取得するときは

storeのstateを取得
const item = useSelector(getItem("item-001"));

変更するときは

storeのstateを変更
const dispatch = useDispatch() ;
//変更したいところで
dispatch(setTitle("ゲームのプログラム"));

としますが、逐一useSelector...と書くのは面倒なのでカスタムhook化しちゃいましょう!
カスタムhookは各state(items,meta,app)ごとにプロジェクトルート/src/state名/hooks.tsxに記述しましょう。

itemsのhooks

itemsを利用する場面を考えると

  • アイテムの情報を取得したい
  • アイテムの一覧を取得したい
  • アイテムを追加・更新したい
  • アイテムを削除したい
  • アイテムを複製したい

があります。これらを愚直に書くと

記述量が多い...
//アイテムの情報を取得する
const item = useSelector(getItem("アイテムID")) ;
//アイテムの一覧を取得する
const items = useSelector(getItems()) ;
//アイテムを追加・更新する
const dispatch = useDisptach() ;
dispatch(set("アイテムID",{/*Itemオブジェクト*/}));  //※setはredux/items/actionsからimport
//アイテムを削除する
const dispatch = useDisptach() ;
dispatch(remove("アイテムID"))  //※removeはredux/items/actionsからimport

うぅん。なんだか長くて可読性が...こう書けたらいいのに...

理想
//アイテムを取得
const item = useItem();
//アイテム一覧を取得
const items = useItems() ;
//アイテムを追加・更新、削除、複製する
const {
  setItem,
  removeItem,
} = useItemOperations() ;
setItem("アイテムID",{/*アイテムオブジェクト*/});
removeItem("アイテムID");

ということで、カスタムhookを作っていきましょう。用意するhooksは

  • useItem ... アイテムをアイテムIDを指定して取得
  • useItems ... アイテム一覧を取得
  • useItemOperations ... アイテムの追加・更新、削除、追加などを行う関数を生成

です。

プロジェクトルート/src/items/hooks.tsx
import { useDispatch, useSelector } from "react-redux";
import { remove, set } from "./actions";
import { getItem, getItems,  } from "./selectors";
import { Item, ItemId } from "./types";

export const useItem = (itemId:ItemId)=>{
    const item = useSelector(getItem(itemId));
    return item ;
} ;

export const useItems = ()=>{
    const item = useSelector(getItems());
    return item ;
} ;

export const useItemOperations = ()=>{
    const dispatch = useDispatch() ;
    const setItem = (itemId:ItemId,item:Item)=>{
        dispatch(set({itemId,item}));
    } ;
    const removeItem = (itemId:ItemId)=>{
        dispatch(remove({itemId}))
    } ;
    return {
        setItem,
        removeItem,
    } ;
} ;

これでitems stateを操作するときに記述量を減らすことができます。

metaのhooks

同じ要領でmeta stateのhooksも作成していきましょう。

作成するstate

  • useTitle ... storeState.meta.titleの取得や更新
  • useFlows ... storeState.meta.flowsの取得や更新
プロジェクトルート/src/meta/hooks.tsx
import { useDispatch, useSelector } from "react-redux";
import { getFlows, getTitle } from "./selectors";
import * as actions from "./actions" ;
import { Flow, ItemId } from "redux/items/types";

export const useTitle = ()=>{
    const dispatch = useDispatch() ;
    const title = useSelector(getTitle()) ;
    const setTitle = (title:string)=>{
        dispatch(actions.setTitle({title}))
    } ;
    return {
        title,
        setTitle,
    } ;
} ;

export const useFlows = ()=>{
    const dispatch = useDispatch() ;
    const flows = useSelector(getFlows()) ;
    const addFlow = (flowId:ItemId)=>{
        dispatch(actions.addFlow({flowId}));
    } ;
    const removeFlow = (flowId:ItemId)=>{
        dispatch(actions.removeFlow({flowId}));
    } ;
    return {
        flows,
        addFlow,
        removeFlow,
    } ;
} ;

JSDocコメント

変数や関数の定義時に以下のように書くことでJSDocコメントを書くことができます。/***/で変数、関数の説明文をコメントとしてマークダウンで記述できます。

JSDocコメント

/**
 * # 使い方
 * 
 * ```tsx
 * const {title,setTitle} = useTitle() ;
 * //タイトルの取得
 * console.log( title ) 
 * //タイトルの更新
 * setTitle( "新しいタイトル" )  
 * ```
 */
export const useTitle = ()=>{
...
}

例えばVSCodeでは関数などの呼び出し側で関数名をホバーするとJSDocコメントが表示されます。

hooksでより呼び出しやすくしたのならついでにJSDocコメントも書いてよりよびだしやすいhooksを実装しましょう!

お疲れ様です!ひとまずこれでRedux周りの実装は終わりました!

ただ後々追加でほしいstateなどがあるかもしれないのでその時は適当な位置にコードを追加していきましょう。

質問・指摘などはこちらから

https://zenn.dev/tbsten/scraps/7123b1257c2097