SYM's Tech Knowledge Index & Creation Records

「INPUT:OUTPUT=1:1以上」を掲げ構築する Tech Knowledge Stack and Index. by SYM@設計者足るため孤軍奮闘する IT Engineer.

React ステート管理 (TypeScript コード例)

React ステート管理 (TypeScript コード例)

ステート

ステート:コンポーネントの描画後に更新されるデータ

ステートの管理は上位コンポーネントで行う。下位コンポーネントの操作でステートを更新する場合は、 下位コンポーネントにステート更新用の関数を譲渡、それを実行することで更新を実現する。

// 親
type StarRatingProps = {
  totalStars?: number
}

const StarRating: FC<StarRatingProps> = ({ totalStars = 5 }) => {
  const [selectedStars, setSelectedStars] = useState(0);
  return (
    <>
      {[...Array(totalStars)].map((n, i) => (
        <Star
          key={i}
          selected={selectedStars > i}
          onSelect={() => setSelectedStars(i + 1)} // ステート更新用関数譲渡
        />
      ))}
      <p>{selectedStars} of {totalStars} stars </p>
    </>
  )
}

// 子
type StarProps = {
  selected: boolean,
  onSelect: (event: React.MouseEvent<SVGElement, MouseEvent>) => void
}

const Star: VFC<StarProps> = ({ selected = false, onSelect = fn => fn }) => (
  <FaStar color={selected ? "red" : "grey"} onClick={onSelect} />  // 操作されたらステート更新
);

フォーム

以下実現方法。制御されたコンポーネント推奨。

useRef (制御されていないコンポーネント)

  • DOMノードに直接アクセスする方法
  • 特徴:イミュータブルでもなければ宣言的でもない()
  • 用途:React以外のライブラリとデータをやり取りする場合(はDOMに直接アクセスが必要)
type AddColorFormProps = {
  onNewColor: (title: string, color: string) => void
}

export const AddColorForm: VFC<AddColorFormProps> = ({
  onNewColor = fn => fn
}) => {
  const textTitle = useRef<HTMLInputElement>(null!);
  const hexColor = useRef<HTMLInputElement>(null!);

  const onSubmit: (event: React.FormEvent<HTMLFormElement>) => void = event => {
    event.preventDefault(); // デフォルト動作(submit:POST送信)抑止
    const title = textTitle.current.value;
    const color = hexColor.current.value;
    onNewColor(title, color);
    textTitle.current.value = "";
    hexColor.current.value = "";
  }

  return (
    <form onSubmit={onSubmit}>
      <input ref={textTitle} type="text" placeholder="input title..." required />
      <input ref={hexColor} type="color" required />
      <button>Add Color</button>
    </form>
  );
}

useState (制御されたコンポーネント)

  • ステート経由でDOMにアクセスする

※制御されたコンポーネント内の描画関数内で重い処理の実行は避ける(パフォーマンス劣化に繋がる)

type AddColorFormProps = {
  onNewColor: (title: string, color: string) => void
}

export const AddColorForm: VFC<AddColorFormProps> = ({
  onNewColor = fn => fn
}) => {
  const [title, setTitle] = useState("");
  const [color, setColor] = useState("#000000")

  const onSubmit: (event: React.FormEvent<HTMLFormElement>) => void = event => {
    event.preventDefault(); // デフォルト動作(submit:POST送信)抑止
    onNewColor(title, color);
    setTitle("");
    setColor("#000000");
  }
  const onChangeTitle: (event: React.ChangeEvent<HTMLInputElement>) => void = event => {
    setTitle(event.target.value)
  }
  const onChangeColor: (event: React.ChangeEvent<HTMLInputElement>) => void = event => {
    setColor(event.target.value)
  }

  return (
    <form onSubmit={onSubmit}>
      <input
        value={title} onChange={onChangeTitle}
        type="text" placeholder="input title..." required
      />
      <input
        value={color} onChange={onChangeColor}
        type="color" required
      />
      <button>Add Color</button>
    </form>
  );
}

カスタムフック

制御されたコンポーネントから重複したコード切り出して抽象化できる

上記コードの以下が重複を変更

value={title} onChange={event => {setTitle(event.target.value)}
interface useInputHook {
  (initValue: string): useInputReturns;
}

type useInputReturns = [
  {
    value: string,
    onChange: (event: React.ChangeEvent<HTMLInputElement>) => void
  },
  () => void
]

export const useInput: useInputHook = (initValue: string) => {
  const [value, setValue] = useState<string>(initValue);
  return [
    {
      value,
      onChange: event => setValue(event.target.value)
    },
    () => setValue(initValue)
  ]
}
// コメント: カスタムフック変更前
type AddColorFormProps = {
  onNewColor: (title: string, color: string) => void
}

export const AddColorForm: VFC<AddColorFormProps> = ({
  onNewColor = fn => fn
}) => {
  // const [title, setTitle] = useState("");
  // const [color, setColor] = useState("#000000")
  const [titleProps, resetTitle] = useInput("");
  const [colorProps, resetColor] = useInput("#000000");

  const onSubmit: (event: React.FormEvent<HTMLFormElement>) => void = event => {
    event.preventDefault(); // デフォルト動作(submit:POST送信)抑止
    onNewColor(titleProps.value, colorProps.value);
    // setTitle("");
    // setColor("");
    resetTitle();
    resetColor();
  }
  // const onChangeTitle: (event: React.ChangeEvent<HTMLInputElement>) => void = event => {
  //   setTitle(event.target.value)
  // }
  // const onChangeColor: (event: React.ChangeEvent<HTMLInputElement>) => void = event => {
  //   setColor(event.target.value)
  // }

  return (
    <form onSubmit={onSubmit}>
      <input
        // value={title} onChange={onChangeTitle}
        {...titleProps}
        type="text" placeholder="input title..." required
      />
      <input
        // value={color} onChange={onChangeColor}
        {...colorProps}
        type="color" required
      />
      <button>Add Color</button>
    </form>
  );
}

Reactコンテキスト

  • コンテキスト:中継地を経由せずにステートを伝達する方法
  • コンテキストプロバイダー:データを渡す方 (親要素側)
  • コンテキストコンシューマー:データを読みだす方 (利用コンポーネント側)

メリット

const Contents = () => {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <ProfilePage user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}

ref:

※ 複数のプロバイダーがある場合、フックははるかにクリーンになる傾向がある

基本形

const colors: ColorData[] = buildInitColors();
export const ColorContext = createContext<ColorData[]>([]);

ReactDOM.render(
  <React.StrictMode>
    <ColorContext.Provider value={colors}>
      <App />
    </ColorContext.Provider>
  </React.StrictMode>,
  document.getElementById('root')
);
export const ColorList: VFC<ColorListProps> = () => {
  const colors = useContext(ColorContext);
  if (colors.length === 0) {
    return <div>No Colors. (Add Color)</div>
  }
  return (
    <div>
      {colors.map(color => (
        <Color key={color.id} {...color.toObj()} />
      ))}
    </div>
  );
}

カスタムプロバイダー

基本はコンテキスト&カスタムフックを使う=コンテキストへのアクセスがカスタムフック経由のみになり安全

コンテキスト&ステート併用

コンテキストプロバイダーはデータ変更ができないため、データ変更を可能にする手段。

type ColorProviderProps = {
  children: React.ReactNode
}

export const ColorContext = createContext(undefined!);

export const ColorProvider = (props: ColorProviderProps) => {
  const [colors, setColors] = useState<ColorData[]>(buildInitColors());
  
  const removeColor = (id: string) => {
    const excludeColor = (id: string) => colors.filter(c => c.id !== id);
    setColors(excludeColor(id));
  }
  const addColor = (title: string, color: string) => {
    const newColors = [...colors, new ColorData(title, color)];
    setColors(newColors);
  }

  return (
    <ColorContext.Provider value={{ colors, addColor, removeColor }}>
      {props.children}
    </ColorContext.Provider>
  );
}

※setColors を公開するとどんな操作でも可能になる=バグが混入する可能性。必要な操作を行う関数のみを(value に設定して)公開する

// プロバイダー側:index.tsx
ReactDOM.render(
  <React.StrictMode>
    <ColorProvider>
      <App />
    </ColorProvider>
  </React.StrictMode>,
  document.getElementById('root')
);
// コンシューマー側
export const ColorList: VFC = () => {
  const { colors } = useContext(ColorContext);
  if (colors.length === 0) {
    return <div>No Colors. (Add Color)</div>
  }
  return (
    <div>
      {colors.map(color => (
        <Color key={color.id} {...color.toObj()} />
      ))}
    </div>
  );
}
コンテキスト&カスタムフック

カスタムフックを導入することで、コンテキストをコンシューマーに一切公開することなくデータ共有可能

コンテキストの操作をカスタムフックで隠ぺいする

type ColorContextValues = {
  colors: ColorData[],
  addColor: (title: string, color: string) => void,
  updateRateColor: (id: string, rating: number) => void,
  removeColor: (id: string) => void
}

const ColorContext = createContext<ColorContextValues>(undefined!);
export const useColors = () => useContext(ColorContext);

export const ColorProvider = (props: ColorProviderProps) => {
  // ... 同上のため省略
}
export const ColorList: VFC = () => {
  const { colors } = useColors();
  if (colors.length === 0) {
    return <div>No Colors. (Add Color)</div>
  }
  return (
    <div>
      {colors.map(color => (
        <Color key={color.id} colorData={color} />
      ))}
    </div>
  );
}

ref:

ステートが更新される=ColorProviderコンポーネント全体が再描画 -> コンポーネントツリー全体にデータ更新が反映

まとめ

ロジックをフックに分離する -> 関心の分離 = UI と ロジックを別々に開発/テスト&デプロイ可能