🦁

【React】子コンポーネントは header, footer, li タグは使わない方がいいんじゃないか説

2022/09/15に公開

前提

まだあまり確信のある方針ではないけれど、おそらくこの方針をとった方がうまくいく。
コードレビューしている内に何度か遭遇したので、メモとして書き残す。

以下の方針を前提として設計するので、この方針を持たない場合は関係ないと思う。

  • 子コンポーネント内部で margin を設定しない
  • コンポーネントはできる限り分割する

具体的にはコンポーネントを使い回すことがないような小規模アプリであれば関係ないと思う。

具体的に

header や footer を使いたくなるケースとしては、 <PageHeader /><PageFooter /><ToDoListItem /> などという命名がされているコンポーネントでよく発生する。

<PageHeader /><PageFooter /> を子に持つ <HomePage /> コンポーネントを例として示してみる。

PageHeader コンポーネント例

return (
  <header className="">
    <Logo />
    <nav className="">
      ...
    </nav>
  </header>
);

HomePage コンポーネント例

return (
  <div className="">
    <PagerHeader />
    <MainContent />
    <PagerFooter />
  </div>
);

もし、 <PageHeader /><MainContent /> の間に margin を追加したい場合に取れる選択肢は

  • <PagerHeader /> に css を注入する
  • <PagerHeader /> を新たなタグで囲って css を設定する

というような選択肢を採ることになる。

これらの選択肢を採ると、 css の注入を行うのであれば子コンポーネントに props の追加する必要が出てしまうし、セクショニングコンテンツとしてマークアップしてあるものをさらにセクショニングコンテンツで囲うのはいい方法ではない。

であるので、最初から margin を設定する親コンポーネントで header, footer などのロールも設定するようにしてしまった方が不要な css の注入がなくて済む。

PageHeader コンポーネント例

return (
  <div className="">
    <Logo />
    <nav className="">
      ...
    </nav>
  </div>
);

HomePage コンポーネント例

return (
  <div>
    <header className="">
      <PagerHeader />
    </header>
    <main>
      <MainContent />
    </main>
    <footer className="">
      <PagerFooter />
    </footer>
  </div>
);

子コンポーネント間のスペーシングが親コンポーネントの債務であれば、子コンポーネントのロールの定義も親コンポーネントの債務というのが自然な流れなのだろう。


おまけの eslint ルール

import { TSESLint } from '@typescript-eslint/experimental-utils';

type Options = { ignoreTags?: string[] };

/**
 * no-ignored-tag-wrapper
 */
const noIgnoredTagWrapper: TSESLint.RuleModule<
  'noIgnoredTagWrapper',
  [Options]
> = {
  meta: {
    type: 'suggestion',
    messages: {
      noIgnoredTagWrapper: '{{ message }}',
    },
    schema: [
      {
        type: 'object',
        properties: {
          ignoreTags: {
            type: 'array',
            items: {
              type: 'string',
            },
          },
        },
        additionalProperties: false,
      },
    ],
  },
  create: (context) => {
    return {
      ReturnStatement: function (node) {
        const ignoreTags = context.options[0]?.ignoreTags ?? [];
        const isReturnJsx = node.argument?.type === 'JSXElement';

        if (!isReturnJsx) {
          return;
        }

        // @ts-ignore
        const tag = node.argument?.openingElement?.name.name;

        if (!ignoreTags.includes(tag)) {
          return;
        }

        context.report({
          node: node,
          messageId: 'noIgnoredTagWrapper',
          data: {
            message: `<${tag} /> は親コンポーネントで定義してください。`,
          },
        });
      },
    };
  },
};

module.exports = noIgnoredTagWrapper;
export default noIgnoredTagWrapper;

Discussion