stin's Blog

Fragment をコンポーネントのトップに置くのはアンチパターンではないかと考える


Fragment をコンポーネントのトップに置くな

「Fragment をコンポーネントにトップに置く」というのは下のようなコンポーネントを作成することを指します。

export const MyComponent = () => {
  return (
    <>
      <h1>This is a sample code.</h1>
      <p>
        Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
        incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
        exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute
        irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
        pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
        officia deserunt mollit anim id est laborum.
      </p>
    </>
  );
};

これは、この MyComponent が生成する HTML 要素が h1p のふたつあるということを意味します。 この書き方がアンチパターンではないかということを感じていたので言語化したいと思います。

親コンポーネントは子コンポーネントが何を表示するかを知るべきではない

一言で言えばこれに尽きます。

例えば下のような 2 つのコンポーネントがあるとします。 (CSS Modules でスタイリングされていると読んでください)

import styles from "./userListStyles.module.css";

export const UserItem = ({ user }) => {
  return (
    <li className={styles.user}>
      <div className={styles.userId}>{user.id}</div>
      <div className={styles.userName}>{user.name}</div>
      <div className={styles.userDescription}>{user.description}</div>
    <li>
  )
}
import styles from "./userListStyles.module.css";

export const UserList = ({ users }) => {
  return (
    <ul className={styles.list}>
      {users.map(user => (
        <UserItem key={user.id} user={user}>
      ))}
    </ul>
  );
};

ul の中に users.length の数だけ li が描画されるため、 HTML の意味としては間違っていません。

ここでは同じ userListStyles.module.css で CSS が当てられていますね。つまり UserItemUserList 専用のコンポーネントのつもりで作成されたと考えられます。

UserListUserItem が トップレベルに li を持つことを知っているから、それを自分の ul の中に配置することができるわけです。

では UserItem コンポーネントを単体で他の div の中で使用したくなったらどうでしょうか。 そのまま UserItem を流用すれば、<div><li /></div> という HTML のセマンティクスに違反するコンポーネントが出来上がってしまいます。

UserItem を修正してトップレベルを div にしたとしても、 既存のコードが <ul><div /></ul> という形になり、やはり HTML のセマンティクスを破壊します。

UserListUserItem を共に div にしちゃえばいいでしょうか?それだと「ユーザーが リストアップ されている」という情報が HTML から消え去ってしまいます。(気にしない人もいるかも知れない)

そもそも HTML において ulli はセットで一つのリストを表示する責務を果たします。責務の観点からすると、これらの Element は分離されるべきではないのではないでしょうか?

つまり、上記の 2 つのコンポーネントはかくあるべきだと。

import styles from "./userInfoStyles.module.css";

export const UserInfo = ({ user }) => {
  return (
    <div className={styles.user}>
      <div className={styles.userId}>{user.id}</div>
      <div className={styles.userName}>{user.name}</div>
      <div className={styles.userDescription}>{user.description}</div>
    <div>
  )
}
import styles from "./userListStyles.module.css";

export const UserList = ({ users }) => {
  return (
    <ul className={styles.list}>
      {users.map(user => (
        <li key={user.id}>
          <UserInfo user={user}>
        </li>
      ))}
    </ul>
  );
};

1 つ目のコンポーネントの名前が UserItem から UserInfo になりました。コンポーネント名 (に加えて TypeScript なら型定義) から、 ユーザー情報を渡せばいい感じに表示してくれて、かつどこでも使えるだろうと推測できます。 CSS も UserList とは別のファイルを参照することで、独立しているのがわかります。

2 つ目のコンポーネントでは、内部で ulli を同時に使用しています。 UserInfoli でラップすることで、 HTML のセマンティクスを保ちつつ、 ユーザー情報の表示を共通化しています。

こうすることで、 UserInfo コンポーネントはユーザー情報を表示する責務、 UserListUserInfo をリストアップする責務、というように責務を一つずつ持つことができます。

親コンポーネントが「子コンポーネントが何の HTML 要素を表示するか」を知っているということは、一つの責務が複数のコンポーネントにまたがっている状態と言えるでしょう。

ちなみに、ここまでで僕が言わんとしていることは TypeScript コミッターで有名な uhyo さん が下記のブログ記事で詳細に解説してくださっているため、僕の文章が下手すぎて伝わらなかった場合は当該記事を参照してください。

本題の場合も、親が「子が Fragment を返すこと」を知っている可能性が高い

ここから本題に入ります。

下記のような Web アプリ全体をグリッド表示するためのコンポーネントをイメージしてください。

import Logo from "components/Logo";
import Header from "components/Header";
import SideMenu from "components/SideMenu";
import Contents from "components/Contents";

export const GridItems = () => {
  return (
    <>
      <Logo />
      <Header />
      <SideMenu />
      <Contents />
    </>
  );
};
export const GridContainer = () => {
  return (
    <div style={{ display: "grid", gridTemplate: "auto 1fr / auto 1fr" }}>
      <GirdItems />
    </div>
  );
};

GridContainer ではインラインスタイルによって 2 * 2 のグリッド表示を構成しています。

GridItemsGridContainer が 2 * 2 であるため、4 つの要素を Fragment で括って返しています。

この時点で既に親子間に依存が発生しているのがわかります。

子である GridItems は、親が GridContainer であり 2 * 2 の grid で待ち受けているから 4 つの要素を返しています。

親である GridContainer は、子の GridItems が Fragment によって 4 つの要素を返すことを知っているから grid 表示ができるのです。

グリッド表示される要素の数をお互いに知っていなければこのようなコンポーネントは出来上がりません。 グリッド表示するというひとつの責務が、複数のコンポーネントにわけられてしまっています。(サンプルが極端な例であることは否めませんが…。)

GridItems のレンダリングの結果、 HTML 要素が間違いなく 4 つならば正しく表示されるでしょう。 ただ、 Fragment をコンポーネントトップに置くことが常態化している場合、HTML 要素がちょうど 4 つであることを保証することに非常に労力をかけなければなりません。

なぜなら Logo, Header, SideMenu, Contents たちもまた、それぞれのトップに Fragment を持っている可能性が出てくるからです。 それらが Fragment をトップにもっていたら、 GridItems の見かけ上は 4 つの要素を返しているように見えるのに、実際の HTML の要素がちょうど 4 つではなくなります。

上記のコンポーネントはこのようにあるべきでしょう。

import Logo from "components/Logo";
import Header from "components/Header";
import SideMenu from "components/SideMenu";
import Contents from "components/Contents";

export const GridContainer = () => {
  return (
    <div style={{ display: "grid", gridTemplate: "auto 1fr / auto 1fr" }}>
      <div>
        <Logo />
      </div>
      <div>
        <Header />
      </div>
      <div>
        <SideMenu />
      </div>
      <div>
        <Contents />
      </div>
    </div>
  );
};

GridItems というコンポーネントは闇に葬りました。そして GridContainer に 4 つの div を設けて LogoHeader などを入れています。 この構成ならば、仮に SideMenu が複数の HTML 要素を生成したとしてもグリッド表示が崩れることはなく、 GridContainer の「グリッド表示」という責務を果たすことができます。

またグリッドのセルの数が減って Logo を表示する必要がなくなった場合でも、下記のように修正すれば対応できます。

import Header from "components/Header";
import SideMenu from "components/SideMenu";
import Contents from "components/Contents";

export const GridContainer = () => {
  return (
    <div style={{ display: "grid", gridTemplate: "auto 1fr / auto 1fr" }}>
      <div style={{ gridColumn: "1 / -1" }}>
        <Header />
      </div>
      <div>
        <SideMenu />
      </div>
      <div>
        <Contents />
      </div>
    </div>
  );
};

このように考えていくと、各コンポーネントのトップには Fragment を置くべきではないと感じられるのではないでしょうか。

Fragment をトップに置いていい場合は?

例えばモーダルを表示する場合なんかが考えられます。

import { useState } from "react";
import Modal from "components/Modal";

export const MyComponent = () => {
  const [open, setOpen] = useState(false);

  return (
    <>
      <div>
        <p>ボタンを押して削除します</p>
        <button onClick={() => setOpen(true)}>削除ボタン</button>
      </div>
      <Modal open={open} onClose={() => setOpen(false)}>
        <DeleteConfirm />
      </Modal>
    </>
  );
};

React でモーダル画面を作成する場合は react-modal などのライブラリを使用していることが多いでしょう。 正しく作られたライブラリならば、モーダルは通常別の場所 (<body> の直下など) にレンダリングされるようになっています。

ですので、上記の MyComponent が実際に生成する HTML は div 一つだけになることがわかります。

まとめ

この記事では Fragment がコンポーネントのトップに置くのはアンチパターンではないかということを考察してきました。

この記事を書いたきっかけは、先日 Qiita で「余分な div を除いて Fragment に書き換えるのが上級者だ」的な記事でトップにあった <div><> に書き換えるのを見かけて考えていたことです。 コンポーネントの責務が何であるかを考えて設計すれば、トップに Fragment を置く機会はかなり少なくなるかと思います。

この記事がみなさんのコンポーネント設計の足掛かりになれば幸いです。