0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

React.ComponentPropsを使ったコンポーネントの Props 設計

Posted at

はじめに

汎用的なElementレイヤーのコンポーネントを作るときの Props定義はこうした方がよいのではないか、という話です。

※ここで言うElementレイヤーとは: input, button, label など、atomic design でいう atom の部分

ComponentPropsを使おう

このように一つ一つのプロパティを定義するより

input.tsx
type Props = {
  value?: string;
  onChange?: React.ChangeEventHandler<HTMLInputElement>;
};
export const Input = ({ value, onChange }: Props) => (
  <input value={value} onChange={onChange} />
);

ComponentPropsを利用して
下記のようにした方がよい、ということです。
(element が持つデフォルトの props を全て受け取れるように)

input.tsx
// 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のみを受け取れるコンポーネントとして作成

input.tsx
type Props = {
  value?: string;
  onChange?: React.ChangeEventHandler<HTMLInputElement>;
};
export const Input = ({ value, onChange }: Props) => (
  <input value={value} onChange={onChange} />
);

typeonBlurを利用する要件に対応させたくなった場合
propsを追加する必要があり、都度コンポーネントに手を加える必要があります。

input.tsx
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を使うと

typeonBlurを利用する要件に対応させたくなった場合も
その都度propsを追加する必要がなくなります。

input.tsx
/**
 * 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 では特定の振る舞いを強制させたいとき

input.tsx
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 が優先される
    />
  );
};

より実践的な使い方

より実践的な使い方として下記のようなインプットコンポーネントを例に考えてみます。

デフォルト
スクリーンショット 2023-02-12 13.34.53.png

エラー
スクリーンショット 2023-02-12 13.35.11.png

先ほどとは異なり、コンポーネント独自のPropsをもたせる必要がでてくるかと思います。

input.tsx
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>
  );
};

コンポーネントを利用するとき

index.tsx
const Index = () => {
  const [value, setValue] = useState('');
  return (
    <div>
      <InputComponent
        title="タイトル"
        errorMessage="エラーメッセージ"
        inputElementProps={{
          value,
          onChange: (e) => setValue(e.target.value),
        }}
      />
    </div>
  );
};

Element がもつ props の仕様を変更したい場合

さらに、inputvalue typepasswordemail だけに限定させたい、というユースケースがあったとします。

その場合、inputが持つデフォルトのpropsであるtypeをコンポーネントで独自に型定義して、InputElementPropstypeを除いた型にします。

input.tsx
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>
  );
};

コンポーネントを利用するとき

index.tsx
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>
  );
};

最後に

自分自身もコンポーネント設計は、現状迷いながら実装している部分が多いです!
こうした方がよいのでは?などあれば気軽にコメントお待ちしております!

最後までお読みいただきありがとうございました!

参考資料

ありがとうございました。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?