Reactおじさんブログ

ReactのComponentProps型を使って不要なPropsとお別れしよう!!

日付更新日サムネイル

どうもReactおじさんです。

ブログをリリースして満足していましたが、記事を執筆しないと価値が無くなってしまうので、だらけずにアプトプットしていこうと思います。

今回は汎用コンポーネントの作り方です。

一般的な汎用コンポーネントの作り方

フロントエンドエンジニアであればコンポーネント作成は避けて通れない道ですよね、、

今回のメインに行く前に一般的に作られているコンポーネントの作り方をおさらいしましょう。

ボタンコンポーネントの場合

import type { FC, ReactNode } from "react";

type Props = {
  children: ReactNode;
  onClick: () => void;
};

export const Button: FC<Props> = ({ children, onClick }) => {
  return <button onClick={onClick}>{children}</button>;
};


テキストフィールドコンポーネントの場合

import type { ChangeEventHandler, FC } from "react";

type Props = {
  value: string | number;
  onChange: ChangeEventHandler<HTMLInputElement>;
};

export const TextFiled: FC<Props> = ({ value, onChange }) => {
  return <input type="text" value={value} onChange={onChange} />;
};


ボタンコンポーネントはクリックが可能。
テキストフィールドコンポーネントは入力が可能な最低限の機能を備えています。

では次に、ボタンコンポーネントにボタンのタイプ、活性 or 非活性の機能を追加してみましょう。

import type { FC, ReactNode } from "react";

type Props = {
  children: ReactNode;
  onClick: () => void;
  disabled: boolean; // 追加
  type: "button" | "reset" | "submit"; // 追加
};

export const Button: FC<Props> = ({ children, onClick, type, disabled }) => {
  return (
    <button type={type} onClick={onClick} disabled={disabled} >
      {children}
    </button>
  );
};


disabledとtypeを追加しました。
上記の様に何か機能を追加したい時は、ボタンコンポーネントの修正・ボタンコンポーネントを呼び出している親コンポーネントの修正が必要になります。

一見普通に見えますが、テキストフィールドコンポーネントの場合id、onBulr、name、onKeyDown等の要素やイベントを必要になった時に追加していくのはメンテコストがかかるし受け取るPropsの型定義が冗長になっていきます。

そこで次に出てくるComponentProps型が救世主となります。

ComponentProps型を使用した汎用コンポーネントの作り方

ComponentProps型とは、簡単に説明すると指定したコンポーネントのProps型を取得できる型です。

↓こんな感じ

type Props = ComponentProps<typeof Button>;




例えば、先ほどのボタンコンポーネントをComponentProps型に指定すると、
ボタンコンコーネントのPropsの型を全て取得できます。

では、先ほどのボタン・テキストフィールドコンポーネントを修正していきます。

ボタンコンポーネントの場合(ComponentProps ver)

import type { FC, ReactNode, ComponentProps } from "react";

type Props = {
  children: ReactNode;
} & ComponentProps<"button">;

export const Button: FC<Props> = ({ children, ...props }) => {
  return <button {...props}>{children}</button>;
};


親コンポーネント

import { Button } from "../components/button";

const Index = () => {
  const onClickHandler = () => console.log("click");

  return (
    <Button type="button" onClick={onClickHandler}>
      テスト
    </Button>
  );
};

export default Index;


テキストフィールドコンポーネントの場合(ComponentProps ver)

import type { ComponentProps, FC } from "react";

type Props = ComponentProps<"input">;

export const TextFiled: FC<Props> = ({ ...props }) => {
  return <input {...props} />;
};


親コンコーネント

import { ChangeEvent, useState } from "react";
import { TextFiled } from "../components/textFiled";

const Index = () => {
  const [value, setValue] = useState("");

  const onChangeHandler = (e: ChangeEvent<HTMLInputElement>) =>
    setValue(e.target.value);

  return <TextFiled value={value} onChange={onChangeHandler} />;
};

export default Index;


順番に見ていきましょう。

  1. ボタン、テキストフィールドコンポーネント両方にComponentPropsを追加
  2. 受け取るPropsをスプレット構文で一括に受け取れるように修正
  3. 受け取ったPropsを各要素に追加


まず1ですが、ComponentProps型を追加してボタンの要素(type,onClick,disabled等)、テキストフィールドの要素(type,onChange,value等)の型を全て受け取っています。

そして2と3で受け取ったPropsを展開していている形になっています。

これで親コンポーネントからPropsを渡してもボタン、テキストフィールドコンポーネントは修正せずに済むのでメンテコストがかからずコード量も削減できました。

ただし、Reactのinput要素はonChange、valueが必須なのでその場合は、下記の様にOmitでComponentPropsから必要な型は除外して、明示的にPropsの型定義を書くことでバグを回避するようにしましょう。

import type { ChangeEventHandler, ComponentProps, FC } from "react";

type Props = {
  value: string | number;
  onChange: ChangeEventHandler;
} & Omit<ComponentProps<"input">, "value" |  "onChange" >;

export const TextFiled: FC<Props> = ({ value, onChange, ...props }) => {
  return <input {...props} value={value} onChange={onChange} />;
};


まとめ

  • ComponentProps型は指定したコンポーネントの型を全て取得できる
  • 普通のHTMLタグの振る舞いをしてほしい場合は、ComponentPropsで型定義した方がメンテコスト、ソース量が削減できる
  • コンポーネントに必須な型はOmit等でComponentPropsから除外し、明示的にPropsの型定義をしてバグを防ぐ



公式ドキュメントに載っていなかったので理解するのに時間がかかりました、、

間違っている部分、分かりにくい部分はTwitterでDMお願いします。