LoginSignup
1
0

More than 1 year has passed since last update.

React Context + useState+ TypeScript を使ってササッとグローバルステート管理をする

Last updated at Posted at 2022-12-02

状態管理ライブラリを使わずやってみよう

Hooks APIができてからあんまりReact触ってないくらいには遠ざかっていたので、初歩的な話題ではありますがグローバルのステート管理をどうするのか復習してみました。

現在はちょっと用意したいだけなら外部の状態管理ライブラリを導入しなくてもそれなりに管理できるようでしたので、Hooks APIのみでやっていきたいと思います。

管理したいデータ構造を決める

まずは、管理したい構造体を明記していきます。

ここはいったんReact非依存で書いておきます。


// Globalで管理したい構造体
export type UIState = {
  readonly user? : {
    name : string
    mail : string
  }
  readonly login : boolean
  readonly currentTime : number
  readonly updatedCount : number
}

// 初期化ファクトリ
export const newUIState = () : UIState => {
  return {
    currentTime: 0, login: false, updatedCount: 0
  }
}

// 変更アクセサ (interface)
type ChangeUIState = {
  setUser(name : string , mail : string) : ChangeUIState
  setLogin(b : boolean) : ChangeUIState
  setCurrentTime(t : number) : ChangeUIState
  update(): UIState
}

// 変更アクセサ (実装)
export const changeUIState = (prev: UIState) : ChangeUIState => {

  return  {
    setUser(name: string, mail: string): ChangeUIState {
      return changeUIState({...prev , user : {name , mail}})
    },
    setLogin(b: boolean): ChangeUIState {
      return changeUIState({...prev , login : b})
    },
    setCurrentTime(t: number): ChangeUIState {
      return changeUIState({...prev , currentTime : t})
    },
    update(): UIState {
      return {...prev , updatedCount : prev.updatedCount +1}
    }
  }

}

immutableな変更メソッドを用意するのに趣味でbuilderパターンっぽくしてますが、ここは何でも良い気はします。(class使わないだのなんだの最近はいろいろあるようですが、どうせ一定規模以上はimmerとかつかうだろう、とかあるし)

管理したいデータ構造← → Reactの橋渡しを作る

import {createContext, Dispatch, ReactNode, SetStateAction, useContext, useState} from "react";
import {newUIState, UIState} from "./state";

// React.Contextで管理したい構造体
type UiStateContextItem = {
	// データ構造そのもの
  state: UIState;
	// データ構造変更のためのuseStateなHook
  setState: Dispatch<SetStateAction<UIState>>;
};

// Contextの識別子
// 初期値宣言するしかない...?
export const UIStateContext = createContext<UiStateContextItem | null>(null);

// Context使用側のHook
export const useUIStateContext = (): [
  UIState,
  Dispatch<SetStateAction<UIState>>
] => {
  const uiCtx = useContext(UIStateContext)!;

  return [uiCtx.state, uiCtx.setState];
};

// state = UiStateContextItem の初期化
const newContextItem = (): UiStateContextItem => {
  const [state, setState] = useState<UIState>(newUIState);

  return {
    state,
    setState,
  };
};

// Context宣言側のHook
export const UIStateContextProvider = (props: {children: ReactNode}) => {
  return <UIStateContext.Provider value={newContextItem()}>{props.children}</UIStateContext.Provider>;
};

React.Contextで管理する値には構造体そのものとは別に、更新系メソッドを設定しておきます。 つまりここではここではuseStateの型と同義になっています。

使ってみる

import {changeUIState, } from "./state";
import {UIStateContextProvider, useUIStateContext} from "./state-react";

export const DemoApp = ()=> {
  return (
    <UIStateContextProvider>
      <WidgetA />
      <WidgetB />
    </UIStateContextProvider>
  );
}

export const WidgetA = ()=> {
  const [state] = useUIStateContext()

  return (
    <div>
      <h1>A</h1>
      <div>{state.currentTime}</div>
    </div>
  );

}

export const WidgetB = ()=> {
  const [state , setState] = useUIStateContext()

  const changeCurrentTime = (currentTime : number)=> {
    setState(prevState => {
      return changeUIState(prevState).setCurrentTime(currentTime).update()
    });
  }

  return (
    <div>
      <h1>B</h1>
      <input
        type="number"
        value={state.currentTime}
        onChange={e => changeCurrentTime(Number(e.target.value))}
      />
    </div>
  );

}

WidgetB コンポーネントから変更したデータがWidgetAにも反映されるようになりました。

グローバルステート管理と銘打ってますが、単なるリアクティブなシングルトン、というわけでもありません。

あくまでDemoAppコンポーネントが所有しているローカルステートに対してReact.Contextを通じてバケツリレーをせずにアクセスできる、といった感覚が正しいかと思います。

なので、DemoAppコンポーネントの破棄とともに管理している状態も掃除できます。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0