🧤

【React】汎用コンポーネントにはReactComponentPropsを使おう

2023/02/12に公開

概要

Reactでbuttoninputなどの汎用的なコンポーネントを作る際にどのようなProps設計がいいかを考えていたところReactにComponentPropsという型定義があるみたいなので使ってみました。

以下のようなPropsの型を定義するとメンテナンスのコストがかかります。

import { VFC, ChangeEventHandler } from "react";

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

const Input:VFC<Props> = ({value, onChange}) => {
    return (
        <input value={value} onChange={onChange} className={style.input} />
    )
}
export default Input

この場合、のちにtypedisabledなどを追加したい場合、親コンポーネントと子コンポーネントでの変更が必要になってしまう。
このようなコンポーネントでは主にUI部分(style)を共通化することが目的であることが多いと思いますので、styleを固定としてあとは汎用的なPropsを受け取れるのが理想であると考えられます。

React.ComponentProps

@types/reactComponentPropsという以下のような型定義があります。

type ComponentProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> =
        T extends JSXElementConstructor<infer P>
            ? P
            : T extends keyof JSX.IntrinsicElements
                ? JSX.IntrinsicElements[T]
                : {};

interface IntrinsicElementsにはずらーとHTMLタグが取りうるPropsが定義されております。

interface IntrinsicElements {
            // HTML
            a: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;
            abbr: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
            address: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
            area: React.DetailedHTMLProps<React.AreaHTMLAttributes<HTMLAreaElement>, HTMLAreaElement>;
            article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
            aside: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
            audio: React.DetailedHTMLProps<React.AudioHTMLAttributes<HTMLAudioElement>, HTMLAudioElement>;
            b: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
            base: React.DetailedHTMLProps<React.BaseHTMLAttributes<HTMLBaseElement>, HTMLBaseElement>;
	......
	......
	......

この型定義を使う際はreactから以下のようにimportしてPropsの型定義を作ることができます。

import {ComponentProps} from 'react'

type Props = ComponentProps<'input'>

あとはこれを分割代入で展開すると汎用的なコンポーネントができあがります。

const Input:VFC<Props> = (props: Props) => {

    return (
        <input {...props} className={style.input} />
    )
}

PickとOmitを使っての制御

TypeScriptのPickとOmitを使用することも可能ですが、このような場合Pickを使うことはあまりないと思います。
Omitを使った型の排除

type Props = Omit<ComponentProps<'input'>, 'className'>

上記はinputタグのComponentPropsからclassNameを排除しています。こうすることでClassNameを固定として親から変更されないようにすることができます。

Omitを使用した上で厳密な型をPropsでもらうようにする
例えばこのinputタグのtypenumbertextのみを親から渡してもらいたいといった要件があるとします。
その場合、OmitでComponentPropsからtypeを排除した上でProps型定義を追加します。

type Input = Omit<ComponentProps<'input'>, 'type'>
type Props = {
    type: 'number' | 'text',
    input: Input
}
const Input:VFC<Props> = ({type, input}) => {

    return (
        <input type={type} {...input} className={style.input} />
    )
}

独自のPropsはいくらでも増やすことができます。よくあるのはエラーメッセージなどだと思います。

type Input = Omit<ComponentProps<'input'>, 'type'>
type Props = {
    type: 'number' | 'text',
    error?: string
    input: Input
}
const Input:VFC<Props> = ({type, error, input}) => {

    return (
        <div>
            <input type={type} {...input} className={style.input} />
            {error && <p>{error}</p>}
        </div>
    )
}

参考
とても参考になりました!
React.ComponentPropsを使ったコンポーネントの Props 設計

Discussion