Ccmmutty logo
Commutty IT
0 pv9 min read

[React]contextを使った状態管理

https://cdn.magicode.io/media/notebox/512fc7c2-a515-40c2-a61c-65e46724646b.jpeg
最近、Dioxusの状態管理の仕組みについて興味を持ち、generational_boxをチラ見しましたが、完全に思考停止してしまったのでとりあえずReactのコンテキストについて思い出すことから始めようと思いこの記事を書いています。

テンプレート

適当にカウンターコンポーネントを作成。
import React, { useState, Dispatch, SetStateAction } from "react";

// ユーティリティ関数と型定義
export type State<T> = [T, Dispatch<SetStateAction<T>>];
export type StateObj<T> = { get: T; set: Dispatch<SetStateAction<T>> };
export type StateArgs<T> = State<T> | StateObj<T>;

export const State = (() => {
  return {
    from: <T,>(args: StateArgs<T>): State<T> => {
      if (Array.isArray(args)) return args;
      return [args.get, args.set];
    },
  };
})();


const CounterComponent: React.FC = () => {
  const countState = useState<number>(0);

  const [countObj, setCountObj] = useState<number>(10);

  // ユーティリティ関数で状態を取得
  const [count, setCount] = State.from(countState); // タプル形式
  const [countFromObj, setCountFromObj] = State.from({
    get: countObj,
    set: setCountObj,
  }); // オブジェクト形式を useState に変換

  return (
    <div>
      <div>タプル形式のカウンター</div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>

      <div>オブジェクト形式のカウンター</div>
      <p>Count from Object: {countFromObj}</p>
      <button onClick={() => setCountFromObj(countFromObj + 1)}>
        Increment Count (Object)
      </button>
    </div>
  );
};

export default CounterComponent;
export const State = (() => {...}の部分について
受け取りデータ形式がStateArgs<T>でタプル形式・オブジェクト形式のどちらの場合にも、戻り値の形式をタプルで統一するために使う。
基本的にデータの状態はタプルである方が良いとされているが、それに加え、状態を操作するロジックを単純化する機能も担う。

Context作成

// src\context\counterContext.ts
import { createContext, Dispatch, SetStateAction, useContext } from "react";


type CounterContextProps = {
    count: number;
    setCount: (
      Dispatch<SetStateAction<number>>
    );
    countFromObj:number;
    setCountFromObj: (
      Dispatch<SetStateAction<number>>
    );
  };


export const CounterContext = createContext<CounterContextProps>({
    count: 0,
    setCount: () => {},
    countFromObj:10,
    setCountFromObj: () => {},
  });


export const useCounterContext = () => useContext(CounterContext);
ここでstateの初期状態を定義。

型 SetStateAction/Dispatch について

type SetStateAction<S> = S | ((prevState: S) => S);
SetStateActionは、値の更新をする
type Dispatch<A> = (value: A) => void;
Dispatchは関数の更新をする
わかりにくいが、SetStateActionでは値や型のみの更新で状態を更新する機能は有していない。DispatchsetStateActionの関数の状態を更新する。

Provider作成

// src\provider\counterProvider.tsx
import { Dispatch, ReactNode, SetStateAction, useState } from "react";
import { CounterContext } from "../context/counterContext";


// ユーティリティ関数と型定義
export type State<T> = [T, Dispatch<SetStateAction<T>>];
export type StateObj<T> = { get: T; set: Dispatch<SetStateAction<T>> };
export type StateArgs<T> = State<T> | StateObj<T>;


export const State = (() => {
    return {
        from: <T,>(args: StateArgs<T>): State<T> => {
        if (Array.isArray(args)) return args;
        return [args.get, args.set];
        },
    };
})();


export const CounterProvider = ({ children }: { children: ReactNode }) => {
    const countState = useState<number>(0);
    const [countObj, setCountObj] = useState<number>(10);

    // ユーティリティ関数で状態を取得
    const [count, setCount] = State.from(countState); // タプル形式
    const [countFromObj, setCountFromObj] = State.from({
        get: countObj,
        set: setCountObj,
    }); // オブジェクト形式を useState に変換
    
    return (
      <CounterContext.Provider value={{
        count: count,
        setCount,
        countFromObj:countFromObj,
        setCountFromObj,
      }}
      >
        {children}
      </CounterContext.Provider>
    );
  };

カウンターコンポーネント修正

// src\components\Counter.tsx
import React from "react";
import { useCounterContext } from "../context/counterContext";


const Counter: React.FC = () => {
  const { count, setCount, countFromObj, setCountFromObj } = useCounterContext();

  return (
    <div>
      {/* タプル形式の状態管理 */}
      <div>タプル形式のカウンター</div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>

      {/* オブジェクト形式の状態管理 */}
      <div>オブジェクト形式のカウンター</div>
      <p>Count from Object: {countFromObj}</p>
      <button onClick={() => setCountFromObj(countFromObj + 1)}>
        Increment Count (Object)
      </button>
    </div>
  );
};

export default Counter;

プロバイダーの呼び出し

とりあえず、トップレベルをラップしておく
// src\App.tsx
import './App.css'
import { CounterProvider } from './provider/counterProvider'
import Counter from './components/Counter'

function App() {
  return (
    <>
      <CounterProvider>
       <Counter />
     </CounterProvider>
    </>
  )
}

export default App
多分ここまでは、誰が書いても同じようなコードになるはず。せっかくなので、もう少し工夫してみる。

追加

src\provider\counterProvider.tsxで使っているユーティリティ関数Stateを編集する。
// src\types\State.ts
import { Dispatch, SetStateAction } from "react";


export type State<T> = [T, Dispatch<SetStateAction<T>>];
export type StateObj<T> = { get: T; set: Dispatch<SetStateAction<T>> };
export type StateArgs<T>
  = State<T>
  | StateObj<T>;
export type OptionalState<T>
  = State<T>
  | [undefined, undefined];
export type OptionalStateArgs<T>
  = OptionalState<T>
  | StateObj<T>
  | undefined;

export const State = (() => {
  return {
    optionalFromArgs: <T>(
      optional: OptionalStateArgs<T>,
    ): OptionalState<T> => {
      if (Array.isArray(optional)) return optional;
      if (optional == null) return [undefined, undefined];
      return [optional.get, optional.set];
    },
    from: <T>(
      args: StateArgs<T>,
    ): State<T> => {
      if (Array.isArray(args)) return args;
      return [args.get, args.set];
    },
  };
})();
OptionalState<T>: undefinedを許容する型を追加。 これで部分的にnullが入っているデータにも対応可能。
合わせてProviderを修正
// src\provider\counterProvider.tsx
export const CounterProvider = ({ children }: { children: ReactNode }) => {
    const countState = useState<number>(0);
    const [countObj, setCountObj] = useState<number>(10);

    // ユーティリティ関数で状態を取得
    const [count, setCount] = State.from(countState); // タプル形式
    const [countFromObj, setCountFromObj] = State.optionalFromArgs({
      get: countObj,
      set: setCountObj,
    }); // オブジェクト形式を useState に変換
    
    return (
      <CounterContext.Provider value={{
        count: count,
        setCount,
        countFromObj:countFromObj || 0, // undefined の場合は 0 を設定
        setCountFromObj: setCountFromObj || (() => {}), // undefined の場合は空の関数を設定
      }}
      >
        {children}
      </CounterContext.Provider>
    );
  };

Discussion

コメントにはログインが必要です。