【React】コンポジションで複数の子コンポーネントを表示する方法

JavaScript

おさらい: 子要素をコンポジションで出力する方法

コンポジションとはコンポーネント間のコードを再利用するためのReactの機能です。
コンポジションではchildrenという特別なPropsを利用して子コンポーネントを出力します。

App.tsx

import { FC } from "react";
import { FancyBorder } from "./FancyBorder";

const App: FC = () => {
  return <FancyBorder>App</FancyBorder>;
};

export default App;

FancyBorder.tsx

import { FC, ReactNode } from "react";
import styles from "./FancyBorder.module.css";

type Props = {
  children: ReactNode;
};

export const FancyBorder: FC<Props> = ({ children }) => {
  return <div className={styles.fancyBorder}>{children}</div>;
};

FancyBorder.module.css

.fancyBorder {
  padding: 10px 10px;
  border: 10px solid;
  border-color: blue;
}

複数の子要素をコンポジションで出力する方法

childrenの代わりに独自のPropsを作成することで複数の子要素を出力できます。
今回は2つ方法を紹介します。

方法1: Reactの要素(ReactNode)をPropsで渡す

以下はheadercontentsというPropsにReactの要素(ReactNode)を渡し、コンポジションで表示する例です。

App.tsx

import { FC } from "react";
import { FancyHeader } from "./FancyHeader";
import { FancyContents } from "./FancyContents";
import { Template } from "./Template";

const App: FC = () => {
  return <Template header={<FancyHeader />} contents={<FancyContents />} />;
};

export default App;

Tamplate.tsx

import { FC, ReactNode } from "react";

type Props = {
  header: ReactNode;
  contents: ReactNode;
};

export const Template: FC<Props> = ({ header, contents }) => {
  return (
    <>
      {header}
      {contents}
    </>
  );
};

FancyHeader.tsx

import { FC } from "react";
import styles from "./FancyHeader.module.css";

export const FancyHeader: FC = () => {
  return <h1 className={styles.fancyHeader}>FancyHeader</h1>;
};

FancyHeader.module.css

.fancyHeader {
  padding: 10px 10px;
  border: 10px solid;
  border-color: red;
}

FancyContents.tsx

import { FC } from "react";
import styles from "./FancyContents.module.css";

export const FancyContents: FC = () => {
  return <div className={styles.fancyContents}>FancyContents</div>;
};

FancyContents.module.css

.fancyContents {
  padding: 10px 10px;
  border: 10px solid;
  border-color: blue;
}

方法2: 関数コンポーネント(React.FC)をPropsで渡す

以下はHeaderContentsというPropsに関数コンポーネント(React.FC)を渡し、コンポジションで表示する例です。

App.tsx

import { FC } from "react";
import { FancyHeader } from "./FancyHeader";
import { FancyContents } from "./FancyContents";
import { Template } from "./Template";

const App: FC = () => {
  return <Template Header={FancyHeader} Contents={FancyContents} />;
};

export default App;

Template.tsx

import { FC } from "react";

type Props = {
  Header: FC;
  Contents: FC;
};

export const Template: FC<Props> = ({ Header, Contents }) => {
  return (
    <>
      <Header />
      <Contents />
    </>
  );
};

参考: 型エイリアスを追加する

コンポジションで出力するコンポーネントに関数型の型エイリアスを追加すると以下のようになります。

Template.tsx

import { FC } from "react";

type Props = {
  Header: FC;
  Contents: FC;
};

+ export type TemplateHeaderComponent = () => FC;
+ export type TemplateContentsComponent = () => FC;

export const Template: FC<Props> = ({ Header, Contents }) => {
  return (
    <>
      <Header />
      <Contents />
    </>
  );
};

FancyHeader.tsx

- import { FC } from "react";
+ import { TemplateHeaderComponent } from "./Template";
import styles from "./FancyHeader.module.css";

- export const FancyHeader: FC = () => {
+ export const FancyHeader: TemplateHeaderComponent = () => () => {
    return <h1 className={styles.fancyHeader}>FancyHeader</h1>;
  };

FancyContents.tsx

- import { FC } from "react";
+ import { TemplateContentsComponent } from "./Template";

- export const FancyContents: FC = () => {
+ export const FancyContents: TemplateContentsComponent = () => () => {
  return <div className={styles.fancyContents}>FancyContents</div>;
};

App.tsx

import { FC } from "react";
import { FancyHeader } from "./FancyHeader";
import { FancyContents } from "./FancyContents";
import { Template } from "./Template";

const App: FC = () => {
-  return <Template Header={FancyHeader} Contents={FancyContents} />;
+  return <Template Header={FancyHeader()} Contents={FancyContents()} />;
};

export default App;

複数の子要素をコンポジションで出力する具体例

複数の子要素をコンポジションで出力する具体例として「ツールチップ」を紹介します。
「マウスオーバーの対象要素」と「マウスオーバーによって表示される要素」の2つをコンポジションで管理します。

App.tsx

import { FC } from "react";
import { Tooltip } from "./shared/Tooltip";
import { Header } from "./Header";
import { HeaderDetail } from "./HeaderDetail";

const App: FC = () => {
  return (
    <Tooltip
      Target={Header({
        title: "Title",
      })}
      Body={HeaderDetail({
        description: "Description",
      })}
    />
  );
};

export default App;

Tooltip.tsx

import { FC, useState } from "react";
import styles from "./Tooltip.module.css";

type Props = {
  Target: FC<TargetProps>;
  Body: FC;
};

type TargetProps = {
  hovered: boolean;
};

export type TooltipTargetFC<T> = (props: T) => FC<TargetProps>;
export type TooltipBodyFC<T> = (props: T) => FC;

export const Tooltip: FC<Props> = ({ Target, Body }) => {
  const [hovered, setHovered] = useState(false);

  return (
    <div
      className={styles.traget}
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
    >
      <Target hovered={hovered} />
      {hovered && (
        <div className={styles.body}>
          <Body />
        </div>
      )}
    </div>
  );
};

Header.tsx

import { TooltipTargetFC } from "./shared/Tooltip";
import styles from "./Header.module.css";

type Props = {
  title: string;
};

export const Header: TooltipTargetFC<Props> =
  ({ title }) =>
  ({ hovered }) => {
    return (
      <div className={styles.title}>
        {title} {!hovered && `(マウスオーバーで詳細表示)`}
      </div>
    );
  };

HeaderDetail.tsx

import { TooltipBodyFC } from "./shared/Tooltip";
import styles from "./HeaderDetail.module.css";

type Props = {
  description: string;
};

export const HeaderDetail: TooltipBodyFC<Props> =
  ({ description }) =>
  () => {
    return <div className={styles.description}>{description}</div>;
  };

さいごに

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!

参考資料