🎁

「Bulletproof React」から学ぶ、FieldWrapperを使って統一レイアウトのフォームを実装する

2023/04/13に公開

先日Bulletproof Reactを読んでいたら便利なコンポーネントがあったので、自分なりの解釈を加えて実装してみました。

https://github.com/alan2207/bulletproof-react

コード、デモが見たい方はこちらからどうぞ。
https://github.com/nyatinte/Zenn_FieldWrapperExample

https://codesandbox.io/p/github/nyatinte/Zenn_FieldWrapperExample/draft/lively-cloud?file=%2FREADME.md&workspace=%257B%2522activeFilepath%2522%253A%2522%252FREADME.md%2522%252C%2522openFiles%2522%253A%255B%2522%252FREADME.md%2522%255D%252C%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522gitSidebarPanel%2522%253A%2522COMMIT%2522%252C%2522spaces%2522%253A%257B%2522clgedf5oh000x3b6iukkff7p1%2522%253A%257B%2522key%2522%253A%2522clgedf5oh000x3b6iukkff7p1%2522%252C%2522name%2522%253A%2522Default%2522%252C%2522devtools%2522%253A%255B%257B%2522type%2522%253A%2522PREVIEW%2522%252C%2522taskId%2522%253A%2522dev%2522%252C%2522port%2522%253A34033%252C%2522key%2522%253A%2522clgedfmhv006d3b6i2jthdcwp%2522%252C%2522isMinimized%2522%253Afalse%257D%255D%257D%257D%252C%2522currentSpace%2522%253A%2522clgedf5oh000x3b6iukkff7p1%2522%252C%2522spacesOrder%2522%253A%255B%2522clgedf5oh000x3b6iukkff7p1%2522%255D%252C%2522hideCodeEditor%2522%253Afalse%257D

FieldWrapperってどんなコンポーネント??

InputSelectなどのフォームコンポーネントをラップするコンポーネントです。

FieldWrapper.tsx
import { FC, ReactNode } from 'react';
import { twMerge } from 'tailwind-merge';

type FieldWrapperProps = {
  label?: string;
  error?: string;
  description?: string;
  className?: string;
  children: ReactNode;
};
export type FieldWrapperPassThroughProps = Omit<
  FieldWrapperProps,
  'children' | 'className'
>;
export const FieldWrapper: FC<FieldWrapperProps> = ({
  label,
  error,
  className,
  description,
  children,
}) => {
  return (
    <div>
      <label className={twMerge('block mb-1', className)}>
        {label}
        {children}
      </label>
      {description && <p className='text-gray-500'>{description}</p>}
      {error && (
        <div role='alert' className='text-red-500'>
          {error}
        </div>
      )}
    </div>
  );
};

twMergeや設計については、私が以前執筆したこちらの記事を参考にしてください。

https://zenn.dev/nyatinte/articles/083ebbe8ab2457

使い方

InputField.tsx
import { forwardRef } from 'react';
import { FieldWrapper, FieldWrapperPassThroughProps } from '../FieldWrapper';
import { Input, InputProps } from './Input';

export type InputFieldProps = InputProps & FieldWrapperPassThroughProps;
export const InputField = forwardRef<HTMLInputElement, InputFieldProps>(
  ({ label, error, description, ...props }, ref) => {
    return (
      <FieldWrapper label={label} error={error} description={description}>
        <Input {...props} ref={ref} />
      </FieldWrapper>
    );
  }
);

InputField.displayName = 'Input';

なんで便利なの?

先程のInputFieldを使ってみましょう。

InputField

いい感じですね、では他のフォームコンポーネントを作成してみましょう。
InputFieldと同じようにSelectFieldを作成してみます。

SelectField.tsx
import { forwardRef } from 'react';
import { FieldWrapper, FieldWrapperPassThroughProps } from '../FieldWrapper';
import { Select, SelectProps } from './Select';

export type SelectFieldProps = SelectProps & FieldWrapperPassThroughProps;
export const SelectField = forwardRef<HTMLSelectElement, SelectFieldProps>(
  ({ label, error, description, ...props }, ref) => {
    return (
      <FieldWrapper label={label} error={error} description={description}>
        <Select {...props} ref={ref} />
      </FieldWrapper>
    );
  }
);

SelectField.displayName = 'Select';

ではSelectFieldを使ってみましょう。
SelectField

先程のInputFieldと並べてみます。

InputFieldとSelectField

レイアウトの責務をFieldWrapperに任せることで、意図しないレイアウト崩れを防ぐことができます。
ここでは省略しますが、TextAreaFieldCheckboxFieldなども同様に作成することができます。

まとめ

いかがでしたでしょうか。
FieldWrapperが便利なコンポーネントであることが少しでも伝われば嬉しいです。

GitHub のリポジトリCodeSandbox を用意しましたので、興味がある方は是非ご覧ください。

https://github.com/nyatinte/Zenn_FieldWrapperExample

https://codesandbox.io/p/github/nyatinte/Zenn_FieldWrapperExample/draft/lively-cloud?file=%2FREADME.md&workspace=%257B%2522activeFilepath%2522%253A%2522%252FREADME.md%2522%252C%2522openFiles%2522%253A%255B%2522%252FREADME.md%2522%255D%252C%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522gitSidebarPanel%2522%253A%2522COMMIT%2522%252C%2522spaces%2522%253A%257B%2522clgedf5oh000x3b6iukkff7p1%2522%253A%257B%2522key%2522%253A%2522clgedf5oh000x3b6iukkff7p1%2522%252C%2522name%2522%253A%2522Default%2522%252C%2522devtools%2522%253A%255B%257B%2522type%2522%253A%2522PREVIEW%2522%252C%2522taskId%2522%253A%2522dev%2522%252C%2522port%2522%253A34033%252C%2522key%2522%253A%2522clgedfmhv006d3b6i2jthdcwp%2522%252C%2522isMinimized%2522%253Afalse%257D%255D%257D%257D%252C%2522currentSpace%2522%253A%2522clgedf5oh000x3b6iukkff7p1%2522%252C%2522spacesOrder%2522%253A%255B%2522clgedf5oh000x3b6iukkff7p1%2522%255D%252C%2522hideCodeEditor%2522%253Afalse%257D

GitHubで編集を提案

Discussion