バイセル Tech Blog

バイセル Tech Blogは株式会社BuySell Technologiesのエンジニア達が知見・発見を共有する技術ブログです。

ヘッダー画像

Reactのchildrenを使用してコンポーネントを拡張した話

はじめに

こちらは バイセルテクノロジーズ Advent Calendar 2022 の 5日目の記事です。

前日の記事は小松山さんの「バッチ処理を Cloud Functions ではなく Cloud Run ジョブで実装した」でした。

こんにちは。テクノロジー戦略本部の22年新卒の玉利です。

現在、私はUIデザインとフロントエンドの業務に携わっています。

最近、フロントエンドの実装でコンポーネントの拡張を行いました。本記事ではモーダルコンポーネントの拡張で保守性の高いコードを書くために気をつけたことを紹介します。

背景

私が所属するプロジェクトでは、もともと図1のように、タイトルとサジェストとボタンを表示することを想定したモーダルコンポーネントが存在していました。

図1: モーダルの説明

しかし、追加要件として図2の右側のモーダルのように、モーダルボディのデザインを変更する必要がありました。モーダルコンポーネントを拡張しようとした場合、既存のモーダルではサジェストのみを表示する仕様だったため、図2の右側のモーダルボディの内容を表示する要件を満たすことができませんでした。

図2: 想定外のケースのモーダル

そのため、要件を満たすためにpropsを追加する必要があると考えました。

しかし、新たにモーダルで表示したい情報が増える度にpropsを都度追加することは、コンポーネントの肥大化を招きます。肥大化の問題点は利用者側のメンテナンスコストがかかることです。全ての利用者側のコンポーネントにおいて、モーダルコンポーネントのpropsの数の増減を追跡し必要に応じて対応をしなければならなくなるためです。

そこで、今回はコンポーネントの肥大化を防ぐためにchildrenを使用して実装しました。 childrenとはコンポーネントの利用者側で制御されたReactNodeを受け取り、コンポーネントの中で展開することで、動的にコンポーネントの中身の表示を変えられるようにする特殊なpropsです。

コンポーネントの拡張

今回の実装ではmuiというコンポーネントライブラリを使用しており、muiを使用したコード例をもとに紹介したいと思います。

最初に作成したモーダルコンポーネントは下記のようなコード例になります。

export const Modal: React.FC<ModalProps> = ({
  title, 
  message, 
  handleClick, 
  buttonLabel,
}) => (
  <Box sx={Style.Modal}>
    <Typography variant="h4">{title}</Typography>
    <Divider />
    <Typography>{message}</Typography>
    <Divider />
    <Button onClick={handleClick}>
      {buttonLabel}
    </Button>
  </Box>  
);

続いて、背景で述べた図2の右側のモーダルボディの内容を表示する要件にpropsの追加で対応する場合、下記のようなコードとなります。

export const Modal: React.FC<ModalProps> = ({
  title,
  message,
  objects,
  handleClick,
  buttonLabel,
}) => (
  <Box sx={Style.Modal}>
    <Typography variant="h4">{title}</Typography>
    <Divider />
    {!!message && <Typography>{message}</Typography>}
    {!!objects && (
      <Stack>
        {objects.map(({key, value}) => (
          <Typography>
            {key}: {value}
          </Typography>
        ))}
      </Stack>
    )}
    <Divider />
    <Button onClick={handleClick}>{buttonLabel}</Button>
  </Box>
);

上記の状態ではそれほど複雑なコンポーネントではないです。しかし、今後も追加要件への対応をpropsを用いて行うと、下記のようにコンポーネントが肥大化する可能性があります。

export const Modal: React.FC<ModalProps> = ({
  title,
  message,
  objects, 
  handleClick, 
  buttonLabel,
  props1,
  ... ,
  props100,
}) => {
  
  ...


  return (
    <Box sx={Style.Modal}>
      <Typography variant="h4">{title}</Typography>
      <Divider />
      {!!message && <Typography>{message}</Typography>}
      {!!objects && (
        <Stack>
          {objects.map(({ key, value }) => (
            <Typography>
              {key}: {value}
            </Typography>
          ))}
        </Stack>
      )}
      {!!props1 && <Typography>{props1}</Typography>}
      ...
      {!!props100 && <Typography>{props100}</Typography>}
      <Divider />
      <Button onClick={handleClick}>
        {buttonLabel}
      </Button>
    </Box>
  );
};

そこで、上記のようなコンポーネントの肥大化を防ぐためにchildrenを使用して実装をしました。 childrenを用いると利用者側のコンポーネントで、モーダルコンポーネントに渡す子要素の構成を自由に記述できます。

そのため、下記のコード例のModalの利用例では<Modal>タグで囲まれた<Stack><Typography>といったReactNodeがchildrenとしてモーダルコンポーネントに渡され、モーダルコンポーネント内の{children}の部分に展開されます。

childrenを使用したモーダルコンポーネントとそれを利用する側のコード例は下記のようになります。

Modal

export const Modal: React.FC<ModalProps> = ({
  title, 
  handleClick, 
  buttonLabel, 
  children,
}) => (
  <Box sx={Style.Modal}>
    <Typography variant="h4">{title}</Typography>
    <Divider />
    {children}
    <Divider />
    <Button onClick={handleClick}>
      {buttonLabel}
    </Button>
  </Box>
);

Modalの利用例

<Modal title="情報の更新" handleClick={() => {}} buttonLabel="更新する">
  <Stack>
    <Typography>情報を更新してよろしいですか?</Typography>
    <Stack sx={Style.Object}>
    {objects.map(({ key, value }) => (
      <Typography>
        {key}: {value}
      </Typography>
    ))}
    </Stack>
  </Stack>
</Modal>

childrenを使用すると利用者側コンポーネントとモーダルコンポーネントが疎結合になり、再利用がしやすく記述をシンプルに保つことができるようになります。

今回はchildrenを使用してコンポーネントを拡張しました。しかし、childrenを使用しないほうが良い場合もあります。そこでモーダルを例にしてchildrenの使いどころとpropsの使いどころを紹介します。

childrenの使いどころ

モーダルボディは表示内容のパターンが無数に存在します。例えば、サジェストの表示、入力フォームの表示、チェックボックスの表示などです。そのため、将来の要件追加によりコンポーネントが肥大化されることが想定されます。この場合はchildrenを使用して実装することで利用者側がモーダルボディの構成を自由に記述できます。なので、モーダルボディのようにパターンが無数に想定される場合にはchildrenで実装するべきだと考えられます。

propsの使いどころ

今回の場合はモーダルヘッダーやモーダルフッターはchildrenで実装せずpropsのみで実装しました。

これらのパーツでは想定されるデザインの変更パターンの数が限られています。例えば、ボタンの数、ボタンのテキスト、ボタンの色を変える といったものです。

もし、childrenを使ってしまうと不必要にデザインの自由度が高くなってしまい、コンポーネントの利用者側がモーダルヘッダーやモーダルフッターのデザインを自由に構成できてしまいます。多く場合、デザインの要件でボタンの色やボタン同士の間隔などが決まっています。なので、このようなケースではchildrenを使わずpropsのみで制御することによってデザインの一貫性を保つ必要があると考えられます。

まとめ

モーダルのコンポーネントを拡張する際、最も避けたかったことはコンポーネントの肥大化です。今回の実装ではchildrenを使用しました。そのため、コンポーネントが肥大化することなく仕様変更に追随しやすい保守性の高いコンポーネントの実装ができました。

また、childrenを使用した実装とpropsのみの実装の使い分けですがデザインの自由度から判断しました。自由度が高ければchildrenで実装する、自由度が低ければpropsのみで実装する、という判断基準です。判断がつかない場合はpropsのみでコンポーネントを作成します。将来の追加要件でコンポーネントを拡張する際に肥大化する恐れがある場合はchildrenを使用して拡張するで良いと考えています。しかし、childrenを使うとどのような要素でも自由にレンダリングできてしまう反面、その自由度の高さからデザインの一貫性を保てなくなってしまう可能性があるため、慎重に検討する必要があります。

今回はReactで実装したためchildrenを使用しましたが、Vueではslotで代用ができるため、同じようなアイデアでコンポーネントが肥大化することなく拡張が実装できます。

最後にバイセルではエンジニアを募集しています。興味がある方はぜひご応募ください!

hrmos.co

明日のバイセルテクノロジーズ Advent Calendar 2022は柴田さんの「AST(抽象構文木)でGoの構造体の定義を検証するツールを作った話」です、そちらもぜひ併せて読んでみてください!