【React】汎用コンポーネントにはReactComponentPropsを使おう
概要
Reactでbutton
やinput
などの汎用的なコンポーネントを作る際にどのような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
この場合、のちにtype
やdisabled
などを追加したい場合、親コンポーネントと子コンポーネントでの変更が必要になってしまう。
このようなコンポーネントでは主にUI部分(style)を共通化することが目的であることが多いと思いますので、style
を固定としてあとは汎用的なPropsを受け取れるのが理想であると考えられます。
React.ComponentProps
@types/react
にComponentProps
という以下のような型定義があります。
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タグのtype
はnumber
とtext
のみを親から渡してもらいたいといった要件があるとします。
その場合、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