はじめに
汎用的なElementレイヤーのコンポーネントを作るときの Props定義はこうした方がよいのではないか、という話です。
※ここで言うElementレイヤーとは: input, button, label など、atomic design でいう atom の部分
ComponentPropsを使おう
このように一つ一つのプロパティを定義するより
type Props = {
value?: string;
onChange?: React.ChangeEventHandler<HTMLInputElement>;
};
export const Input = ({ value, onChange }: Props) => (
<input value={value} onChange={onChange} />
);
ComponentProps
を利用して
下記のようにした方がよい、ということです。
(element が持つデフォルトの props を全て受け取れるように)
// input element が持つ props を全て受け取れるようになる
type Props = React.ComponentProps<'input'>;
export const Input = (props: Props) => <input {...props} />;
ComponentProps
を利用することで、React が提供している Element が持つ Props を全てうけとれるようになります。
ComponentProps
-
ComponentPropsWithRef
:refを許容 -
ComponentPropsWithoutRef
:refを許容しない
※個人的にはref
も許容したい場面が多いのでComponentPropsWithRef
を利用することが多いです。
ComponentPropsを使うメリット
メリットはコンポーネントのメンテナンスコストが軽減されることにあります
ComponentPropsを使わない場合
作成当初はvalue
, onChange
のみを受け取れるコンポーネントとして作成
type Props = {
value?: string;
onChange?: React.ChangeEventHandler<HTMLInputElement>;
};
export const Input = ({ value, onChange }: Props) => (
<input value={value} onChange={onChange} />
);
type
やonBlur
を利用する要件に対応させたくなった場合
props
を追加する必要があり、都度コンポーネントに手を加える必要があります。
type Props = {
value?: string;
onChange?: React.ChangeEventHandler<HTMLInputElement>;
type?: string; // 追加
onBlur?: React.FocusEventHandler<HTMLInputElement>; // 追加
};
export const Input = ({ value, type, onChange, onBlur, onKeyPress }: Props) => (
<input
value={value}
onChange={onChange}
type={type} // 追加
onBlur={onBlur} // 追加
/>
);
ComponentPropsを使うと
type
やonBlur
を利用する要件に対応させたくなった場合も
その都度props
を追加する必要がなくなります。
/**
* React.ComponentProps<'input'>; で
* input要素が持つPropsを全て受け取れるようになる
*
* value?: string;
* type?: string;
* onChange?: React.ChangeEventHandler<HTMLInputElement>;
* onBlur?: React.FocusEventHandler<HTMLInputElement>;
* onFocus?: React.FocusEventHandler<HTMLInputElement>;
* onKeyPress?: React.KeyboardEventHandler<HTMLInputElement>;
* etc...
*/
type InputProps = React.ComponentProps<'input'>;
export const Input = (props: InputProps) => {
// 分割代入で全ての値を渡すことができる
return <input {...props} />;
};
props
を全てそのまま渡すのではなく、特定の props
ではコンポーネント独自の振る舞いをさせたいときは、{...props}
の下にそのprops
を渡します。
ex)onChange
では特定の振る舞いを強制させたいとき
type Props = React.ComponentProps<'input'>;
export const Input = (props: Props) => {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
// このコンポーネントで onChange 発火時に必ず実行したい振る舞いを書く
props.onChange?.(e);
};
return (
<input
{...props} // <- onChange が含まれるものの
onChange={handleChange} // <- こちらの handleChange が優先される
/>
);
};
より実践的な使い方
より実践的な使い方として下記のようなインプットコンポーネントを例に考えてみます。
先ほどとは異なり、コンポーネント独自のPropsをもたせる必要がでてくるかと思います。
type InputElementProps = React.ComponentProps<'input'>;
type Props = {
title: string; // コンポーネント独自のprops
errorMessage?: string; // コンポーネント独自のprops
inputElementProps?: InputElementProps; // inputElementのprops
};
export const InputComponent = ({
title,
errorMessage,
inputElementProps,
}: Props) => {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
// このコンポーネントで onChange 発火時に必ず実行したい振る舞いを書く
inputElementProps?.onChange?.(e);
};
return (
<div style={rootStyle}>
<p style={titleStyle}>{title}</p>
<input {...inputElementProps} onChange={handleChange} />
<p style={errorStyle}>{errorMessage}</p>
</div>
);
};
コンポーネントを利用するとき
const Index = () => {
const [value, setValue] = useState('');
return (
<div>
<InputComponent
title="タイトル"
errorMessage="エラーメッセージ"
inputElementProps={{
value,
onChange: (e) => setValue(e.target.value),
}}
/>
</div>
);
};
Element がもつ props の仕様を変更したい場合
さらに、input
の value type
を password
と email
だけに限定させたい、というユースケースがあったとします。
その場合、input
が持つデフォルトのprops
であるtype
をコンポーネントで独自に型定義して、InputElementProps
はtype
を除いた型にします。
type InputElementProps = Omit<React.ComponentProps<'input'>, 'type'>; // input element の props から type が除外される
type Props = {
title: string;
type: 'password' | 'email'; // password と email に限定(inputのデフォルトでは string型)
errorMessage?: string;
inputElementProps?: InputElementProps; // inputElementのprops
};
export const InputComponent = ({
title,
type,
errorMessage,
inputElementProps,
}: Props) => {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
// このコンポーネントで onChange 発火時に必ず実行したい振る舞いを書く
inputElementProps?.onChange?.(e);
};
return (
<div style={rootStyle}>
<p style={titleStyle}>{title}</p>
<input
{...inputElementProps}
type={type} // ここで渡すのを忘れずに
onChange={handleChange}
/>
<p style={errorStyle}>{errorMessage}</p>
</div>
);
};
コンポーネントを利用するとき
const Index = () => {
const [value, setValue] = useState('');
return (
<div>
<InputComponent
title="タイトル"
errorMessage="エラーメッセージ"
type='password' // password or email しか渡せない
inputElementProps={{
value,
onChange: (e) => setValue(e.target.value),
type: 'number' // error: 除外しているのでここでは渡せない
}}
/>
</div>
);
};
最後に
自分自身もコンポーネント設計は、現状迷いながら実装している部分が多いです!
こうした方がよいのでは?などあれば気軽にコメントお待ちしております!
最後までお読みいただきありがとうございました!
参考資料
ありがとうございました。