🔥

Reactコンポーネントみんな違ってみんな良い、、訳ない

2022/10/30に公開

👩🏻:「わたしはstyled componentで書きました。」
👦🏻:「良いですね!ぼくはCSS Modulesで書きましたよ!」
🧔🏻‍♂️:「みなさんFunctionalなんて今どきですね。私のコンポーネントはすべてClassです。」

さすがにここまで各々自由でバラバラに書くような現場は見たことないですが、やっぱりそれぞれ慣れ親しんだライブラリや好みがある訳で、やりやすい方法で実装したいですよね。
ですがチーム開発においてはやっぱりコードは統一されるべきだと思います。

そこで私のチームではコンポーネントを実装する際はまずコードジェネレーターを使って雛形を作成し、それに沿ってコンポーネントの実装をしていくようにしています。逆に言えばその雛形から大きく外れないような汎用性が必要になります。

今回はそんな雛形を考えているうちにたどり着いたコンポーネントの書き方についてのご紹介になります。

前提知識

以下のワードにピンと来る方であれば問題なく読み進められるかと思います。

  • React
  • TypeScript
  • CSSinJS
  • メモ化
  • ジェネリクス

まずはシンプルな書き方

はじめに今回紹介する書き方との比較のためにサンプルコードを用意しました。titlecontentsをpropsで受け取るArticleコンポーネントになります。

export const Article = (props: {
  title: string;
  contents: string;
}) => {
  return (
    <div>
      <h1>{props.title}</h1>
      <p>{props.contents}</p>
    </div>
  );
};

割とよく見かける書き方な気がします。

続いて今回ご紹介する書き方

import { memo } from "react";
import { css } from "@emotion/react";

type Props = {
  className?: string;
  title: string;
  contents: string;
};

const _Article = ({ className, title, contents }: Props) => {
  const style = createStyle();

  return (
    <div className={className} css={style.root}>
      <h1 css={style.title}>{title}</h1>
      <p css={style.contents}>{contents}</p>
    </div>
  );
};

const createStyle = () => {
  return {
    root: css``,
    title: css``,
    contents: css``,
  };
};

export const Article = memo(_Article) as typeof _Article;

シンプルな方に比べコード量も増えています。なぜこのようなコードに至ったか、シンプルな書き方からリファクタリングしていく形で解説していきたいと思います。

ポイント解説

ポイント①:propsの型を定義する

propsの型はPropsという型名で一番上に定義します。以下のメリットがあります。

  • propsの型というコンポーネントがどのようなものかを示す重要な要素をファイルを開いたらすぐに把握できる。
  • Props=そのコンポーネントのpropsの型」という共通認識が持てる。
+ type Props = {
+   title: string;
+   contents: string;
+ };

- export const Article = (props: {
-   title: string;
-   contents: string;
- }) => {
+ export const Article = (props: Props) => {
    return (
      <div>
        <h1>{props.title}</h1>
        <p>{props.contents}</p>
      </div>
    );
  };

ポイント②:propsは分割代入する

propsは分割代入して受け取ります。以下のメリットがあります。

  • props.titleのような冗長な書き方をせずに済む。
  • 必要であれば初期化も同時に行える。
  type Props = {
    title: string;
    contents: string;
  };

- export const Article = (props: Props) => {
+ export const Article = ({ title, contents = "" }: Props) => {
    return (
      <div>
-       <h1>{props.title}</h1>
-       <p>{props.contents}</p>
+       <h1>{title}</h1>
+       <p>{contents}</p>
      </div>
    );
  };

ポイント③:CSSはJSXの下に書く

CSSはCSSinJSを使い、JSXの下に書きます。以下のメリットがあります。

  • JSXとCSS間のファイルの行き来をなくせる。
  • 触る機会の多いJSXのコードをファイルを開いた際にすぐに把握できる。
+ import { css } from "@emotion/react";
  
  type Props = {
    title: string;
    contents: string;
  };

  export const Article = ({ title, contents = "" }: Props) => {
    return (
-     <div>
+     <div css={style}>
        <h1>{title}</h1>
        <p>{contents}</p>
      </div>
    );
  };
  
+ const style = css``;

今回の例ではemotionを使っていますが、ここで伝えたいのはあくまで「CSSは同ファイル上のJSXの下に書く」です。emotion及びCSSをテンプレートリテラルを使って書くことを推す意図はありません。

また今回のポイントとは関係ありませんが、コンポーネントを使う側から独自のスタイルを当てたい場合、emotionではclassNameをpropsで受け取れるようにしておく必要があります。なのでこのタイミングでpropsにclassNameも追加しておきます。これは使うライブラリによっては必要ありません。

  import { css } from "@emotion/react";
  
  type Props = {
+   className?: string;
    title: string;
    contents: string;
  };

- export const Article = ({ title, contents = "" }: Props) => {
+ export const Article = ({ className, title, contents = "" }: Props) => {
    return (
-     <div css={style}>
+     <div className={className} css={style}>
        <h1>{title}</h1>
        <p>{contents}</p>
      </div>
    );
  };
  
  const style = css``;

ポイント④:各要素のスタイルはファクトリ関数で生成する

createStyleのような各要素のスタイルを返すファクトリ関数を書きます。以下のメリットがあります。

  • 複数の要素で共通して使う値の計算処理なんかをcreateStyleに引数を渡すことでまとめられる。
  • CSSのために必要な処理をJSX内に書かなくて済む。
  import { css } from "@emotion/react";
  
  type Props = {
    className?: string;
    title: string;
    contents: string;
  };
  
  export const Article = ({ className, title, contents = "" }: Props) => {
+   const style = createStyle();

    return (
-     <div className={className} css={style}>
-       <h1>{title}</h1>
-       <p>{contents}</p>
+     <div className={className} css={style.root}>
+       <h1 css={style.title}>{title}</h1>
+       <p css={style.contents}>{contents}</p>
      </div>
    );
  };
  
- const style = css``;
+ const createStyle = () => {
+   return {
+     root: css``,
+     title: css``,
+     contents: css``,
+   };
+ };

ポイント⑤:コンポーネントはメモ化する

メモ化の必要がない場合、無駄なメモ化のコストがかかると反対の意見もありましたが、reactの内部コードを使いコスト検証した結果、このコストは無視しても良いレベルと結論づけました。なのでコンポーネントはすべてメモ化します。以下のメリットがあります。

  • 再レンダリングコストを抑えることができる。
  • パフォーマンス問題が起きた際に、再レンダリングコストの検証をする必要がなくなる。(つまり再レンダリングに時間がかかっているコンポーネントの特定なんかが不要になる。)
+ import { memo } from "react";
  import { css } from "@emotion/react";

  type Props = {
    className?: string;
    title: string;
    contents: string;
  };

- export const Article = ({ className, title, contents = "" }: Props) => {
+ export const Article = memo(({ className, title, contents = "" }: Props) => {
    const style = createStyle();

    return (
      <div className={className} css={style.root}>
        <h1 css={style.title}>{title}</h1>
        <p css={style.contents}>{contents}</p>
      </div>
    );
- };
+ });

  const createStyle = () => {
    return {
      root: css``,
      title: css``,
      contents: css``,
    };
  };

ポイント⑥:React.memoに無名関数を渡すのをやめる

React.memoの引数に無名関数を渡すのではなく、一度_Articleで定義し、それをReact.memoに渡します。以下のメリットがあります。

  • 無名関数を渡していると[React Developer Tools]に_cと表示されてしまいます。これだと自分が見たいコンポーネントが見つけられず使い物になりませんが、_Articleに一度入れることで、ツール上にも_Articleと表示されるようになります。
  import { memo } from "react";
  import { css } from "@emotion/react";

  type Props = {
    className?: string;
    title: string;
    contents: string;
  };

- export const Article = memo(({ className, title, contents = "" }: Props) => {
+ const _Article = ({ className, title, contents = "" }: Props) => {
    const style = createStyle();

    return (
      <div className={className} css={style.root}>
        <h1 css={style.title}>{title}</h1>
        <p css={style.contents}>{contents}</p>
      </div>
    );
- });
+ };

  const createStyle = () => {
    return {
      root: css``,
      title: css``,
      contents: css``,
    };
  };

+ export const Article = memo(_Article);

ちなみに私はコンポーネント名に_が付くことは自分が意図的にメモ化しているコンポーネントの目印にもなるので良いと思っています。ですが中には気持ち悪いと感じる方もいるかもしれません。そんな場合、_Article.displayName = 'Article';というようにdisplayNameを設定し直すことで好きな名前を表示できるようになります。

ポイント⑦:React.memoの返り値の型をコンポーネントの型に上書きする

非常に不本意ながらasを使ってmemo(_Article)の返り値の型を_Articleの型で上書きしています。以下のメリットがあります。

  • ジェネリクスが使いやすくなる。
  import { css } from '@emotion/react';
  import { memo } from 'react';

  type Props = {
    className?: string;
    title: string;
    contents: string;
  };

  const _Article = ({ className, title, contents = '' }: Props) => {
    const style = createStyle();

    return (
      <div className={className} css={style.root}>
        <h1 css={style.title}>{title}</h1>
        <p css={style.contents}>{contents}</p>
      </div>
    );
  };

  const createStyle = () => {
    return {
      root: css``,
      title: css``,
      contents: css``,
    };
  };

- export const Article = memo(_Article);
+ export const Article = memo(_Article) as typeof _Article;

上記コードでジェネリクスを使う場合は以下のようなコードになります。

  import { css } from '@emotion/react';
  import { memo } from 'react';

- type Props = {
-   className?: string;
-   title: string;
+ type Props<T extends string> = {
+   className?: string;
+   title: T;
    contents: string;
  };

- const _Article = ({ className, title, contents = '' }: Props) => {
+ const _Article = <T extends string>({ className, title, contents = '' }: Props<T>) => {
    const style = createStyle();

    return (
      <div className={className} css={style.root}>
        <h1 css={style.title}>{title}</h1>
        <p css={style.contents}>{contents}</p>
      </div>
    );
  };

  const createStyle = () => {
    return {
      root: css``,
      title: css``,
      contents: css``,
    };
  };

  export const Article = memo(_Article) as typeof _Article;

コンポーネントを使う際の例ですが、例えばtitleに渡す型を'PageA' | 'PageB'にしたい場合、<Article<'PageA' | 'PageB'> ... />のように書きます。これでtitleには'PageA' | 'PageB'しか渡せなくなり、それ以外を渡そうとするとしっかり型エラーとなります。

ちなみに

冒頭で述べたコードジェネレータを使えば、ここまではコマンドで生成されます。
なので個々人がわざわざ書き方を覚えたり、ドキュメントを作る必要はありません。むしろそういった手段を選ぶよりもコードジェネレーターを使うことをおすすめします。

import React from 'react';
import { css } from '@emotion/react';

type Props = {
  className?: string;
  // props
};

const _SomeComponent = ({ className }: Props) => {
  const style = createStyle();

  // process

  return (
    <div className={className} css={style.root}>
      {/* jsx */}
    </div>
  );
};

const createStyle = () => {
  return {
    root: css`
      /* style */
    `,
  };
};

export const SomeComponent = React.memo(_SomeComponent) as typeof _SomeComponent;

コードジェネレーターの使い方などについてはまた別途記事にしたいと思います。
https://zenn.dev/onori/articles/d6ec2621a619e1

余談

今回ご紹介した書き方はいくつものバージョンを経て至ったものになります。そして今後も定期的にアップデートが走ると思います。新しいひらめきや技術によってより良い書き方を思いつくようであれば、可能な限り記事にしていきたいと思います。

まとめ

  • CSSは下に書くとSFCっぽくて見やすい。
  • 全部メモ化しちゃえばメモ化の必要性に関する議論、パフォーマンスチューニング時に再レンダリングコストがかかっているコンポーネントの特定及びメモ化対応が必要なくなる。
  • 簡単にコンポーネントにジェネリクスが使えるようになるし、「react memo generic」とかで検索する必要がなくなる。

Discussion