88
35

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

お題は不問!Qiita Engineer Festa 2023で記事投稿!

【React】Context を使う前に #1 無駄なコンポーネントの層を削れ

Last updated at Posted at 2023-06-14

Props のバケツリレー (Props Drilling) を解決するときに、安易に Context を使ったり、状態管理ライブラリ(Recoil, Jotai, Redux)に頼っていませんか?

そんなことをせずとも、「CompA が CompB を使い、CompB が CompC を使い、 CompC が ...」という依存関係のチェーンを浅くするのが最善の解決策である場合があります。

「ローカルとはいえない、真にグローバルな状態1 である」なら、Recoil や Jotai が使えますし、 Context には Context の活躍できる場面2 があります。今回はそれらの話はしません。

KISS (Keep It Simple Stupid) という名言があるように、「Props を渡すだけ」というわかりやすい方法を取ることで、将来のコード読解が楽になり、メンテナンスが容易になり、そもそもバグの混入を防げるので、一石三鳥です。

深すぎる依存関係と浅い依存関係


2つの方法がありますが、まずは前者「そもそも無駄なコンポーネントを挟まない」をこの記事で解説します。

1. そもそも無駄なコンポーネントを挟まない ← 今ここ

2. コンポジション (ReactNode 型 Props) を使う

3. ステートを適切なコンポーネントに置く

このうち、3 については、すでに記事化している内容と重複するので、そちらを参照してください (バケツリレーには言及していませんが、不要な Props の受け渡しが削られていることが明らかだと思います。)

この記事が示すのは方向性であり、サンプルコードを鵜呑みにするべきではありません。スタイリングに使う技術(CSS, CSS in JS, MUI のようなライブラリ, etc.) によってもベストは変わります。

もちろんですが、仕様書・デザインカンプと実際のコードをつぶさに観察しながら「この層は本当に必要なのか、そうでないのか」を検討しましょう。

Not Good 不要なレイヤーだらけ

あなたは、ブログ作成プロジェクトに新しく参画して、既存のコードを理解するために読み進めています。

まずは、記事一覧ページ全体のコンポーネントから初めて、リストの各要素はどのように記述されているのか見てみましょう。

ArticleIndexPage.tsx
const ArticleIndexPage: FC = () => {
  const articles = useSWR(/* 中略 */, { suspense: false });

  return (
    <div>
      <AppHeader />
      <ArticleIndexPageMiddle
        articles={articles.data}
        totalCount={articles.totalCount}
      />
      <AppFooter />
    </div>
  );
};

お、ページの主要部は ArticleIndexPageMiddle ってコンポーネントに書かれてるんやな

ArticleIndexPageMiddle.tsx
type Props = {
  totalCount: number,
  articles: Articles[],
}

const ArticleIndexPageMiddle: FC<Props> = ({
  totalCount,
  articles,
}) => {
  return (
    <main>
      <div>{totalCount}</div>
      <ArticlesArea articles={articles} />
    </main>
  );
};

お?詳しくは ArticlesArea を見ればええんか

ArticlesArea.tsx
type Props = {
  articles: Article[]
}

export const ArticlesArea: FC<Props> = ({ articles }) => {
  return (
    <div>
      {articles.map(item => (
        <ArticleItem 
          key={item.slug}
          title={item.title}
          slug={item.slug}
        />
      ))}
    </div>
  );
};

(ため息)

で、ArticleItem と...

ArticleItem.tsx
type Props = {
  title: string,
  slug: string,
}

export const ArticleItem: FC<Props> = ({
  title,
  slug,
}) => {
  return (
    <article>
      <h2>{title}</h2>
      <a href={`/post/${slug}`}>読む</a>
    </article>
  );
};

これでやっと到達しました。

「swr によるデータ取得 → ArticleItem の各 Props」 の ページ全体のデータの流れ を把握するためには、何度も何度もコードジャンプをしなければならず、「コードベースの全体像 / 詳細をともに把握する」ことが難しくなります。

ArticleIndexPage を起点とした深すぎる依存関係のチェーン

After 無駄な中間層としてのコンポーネントを削除

新しいメンバーが困らないためには、どうすれば良かったのでしょうか?

ここでは、いらないコンポーネントの層を取り払ってしまうのが、一つの解決策としてありえます。 ( ArticleItem のように、リストの各要素のコンポーネントは 必要な抽象化 として役立つので、残してあげました。)

新しくなった ArticleIndexPage を見てみましょう。

ArticleIndexPage.tsx
const ArticleIndexPage: FC = () => {
  const articles = useSWR(/* 中略 */, { suspense: true });

  return (
    <div>
      <AppHeader />
      <main>
        <ArticleIndexSummary totalCount={articles.totalCount} />
        <ArticleIndexItems articles={articles.items} />
      </main>
      <AppFooter />
    </div>
  );
};

export default ArticleIndexPage;

ずいぶん読みやすくなりましたね。ページ本体である ArticleIndexPage を見るだけで 「swr によるデータ取得して、総件数と記事一覧のコンポーネントに渡している」 という データの流れが一望できます し、ArticleItem の詳細な実装にたどり着くのに 必要なコードジャンプが最大で2回だけ になります。

浅く広くなったArticleIndexPage 起点の依存関係チェーン

ArticleIndexSummary.tsx
type Props = {
  totalCount: number,
}

export const ArticleIndexSummary: FC<Props> = () => {
  return <div>{totalCount}</div>;
}
ArticleIndexItems.tsx
type Props = {
  articles: { title: string, slug: string }[],
}

export const ArticleIndexItems: FC<Props> = ({
  articles
}) => {
  return (
    <div>
      {articles.map(item => (
        <ArticleItem 
          key={item.slug}
          title={item.title}
          slug={item.slug}
        />
      ))}
    </div>
  );
}
ArticleItem.tsx
type Props = {
  title: string,
  slug: string,
}

export const ArticleItem: FC<Props> = ({
  title,
  slug,
}) => {
  return (
    <article>
      <h2>{title}</h2>
      <a href={`/post/${slug}`}>読む</a>
    </article>
  );
};

まとめ

深すぎる依存関係と浅い依存関係

このように、見た目上のグループをそのままコンポーネントの入れ子として実装してしまうと、余計な Props の受け渡しが生まれ、必要なコードジャンプも増えてしまいます。フラットに書けるところはフラットに書くことで依存関係のチェーンを浅くすることが大切です。

「とはいっても、スタイルの都合上コンポーネントにまとめないといけなくて、どうしてもコンポーネントのネストが深くなってしまう...」という方は、 コンポジション (composition) を使ってみてはどうでしょうか?

▼ 次回の記事はこちら

関連スライド、メモ

脚注の記事

▼ 「グローバルな状態」は実はほんの少しなのでは?という洞察を掘り進められた記事

▼ Context はグローバルな状態というより、コンポーネントの階層構造を生かした実装に向いてる、という記事

  1. https://zenn.dev/yoshiko/articles/607ec0c9b0408d - 「3種類」で管理するReactのState戦略 by よしこ

  2. https://zenn.dev/neet/articles/f25abb616ec105 - React で h1-h6 を正しく使い分ける by Ryō Igarashi

88
35
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
88
35

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?