Props のバケツリレー (Props Drilling) を解決するときに、安易に Context を使ったり、状態管理ライブラリ(Recoil, Jotai, Redux)に頼っていませんか?
そんなことをせずとも、「CompA が CompB を使い、CompB が CompC を使い、 CompC が ...」という依存関係のチェーンを浅くするのが最善の解決策である場合があります。
KISS (Keep It Simple Stupid) という名言があるように、「Props を渡すだけ」というわかりやすい方法を取ることで、将来のコード読解が楽になり、メンテナンスが容易になり、そもそもバグの混入を防げるので、一石三鳥です。
2つの方法がありますが、まずは前者「そもそも無駄なコンポーネントを挟まない」をこの記事で解説します。
1. そもそも無駄なコンポーネントを挟まない ← 今ここ
2. コンポジション (ReactNode 型 Props) を使う
3. ステートを適切なコンポーネントに置く
このうち、3 については、すでに記事化している内容と重複するので、そちらを参照してください (バケツリレーには言及していませんが、不要な Props の受け渡しが削られていることが明らかだと思います。)
この記事が示すのは方向性であり、サンプルコードを鵜呑みにするべきではありません。スタイリングに使う技術(CSS, CSS in JS, MUI のようなライブラリ, etc.) によってもベストは変わります。
もちろんですが、仕様書・デザインカンプと実際のコードをつぶさに観察しながら「この層は本当に必要なのか、そうでないのか」を検討しましょう。
Not Good 不要なレイヤーだらけ
あなたは、ブログ作成プロジェクトに新しく参画して、既存のコードを理解するために読み進めています。
まずは、記事一覧ページ全体のコンポーネントから初めて、リストの各要素はどのように記述されているのか見てみましょう。
const ArticleIndexPage: FC = () => {
const articles = useSWR(/* 中略 */, { suspense: false });
return (
<div>
<AppHeader />
<ArticleIndexPageMiddle
articles={articles.data}
totalCount={articles.totalCount}
/>
<AppFooter />
</div>
);
};
お、ページの主要部は ArticleIndexPageMiddle ってコンポーネントに書かれてるんやな
type Props = {
totalCount: number,
articles: Articles[],
}
const ArticleIndexPageMiddle: FC<Props> = ({
totalCount,
articles,
}) => {
return (
<main>
<div>{totalCount}件</div>
<ArticlesArea articles={articles} />
</main>
);
};
お?詳しくは ArticlesArea を見ればええんか
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 と...
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」 の ページ全体のデータの流れ を把握するためには、何度も何度もコードジャンプをしなければならず、「コードベースの全体像 / 詳細をともに把握する」ことが難しくなります。
After 無駄な中間層としてのコンポーネントを削除
新しいメンバーが困らないためには、どうすれば良かったのでしょうか?
ここでは、いらないコンポーネントの層を取り払ってしまうのが、一つの解決策としてありえます。 ( ArticleItem のように、リストの各要素のコンポーネントは 必要な抽象化 として役立つので、残してあげました。)
新しくなった ArticleIndexPage を見てみましょう。
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回だけ になります。
type Props = {
totalCount: number,
}
export const ArticleIndexSummary: FC<Props> = () => {
return <div>{totalCount}件</div>;
}
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>
);
}
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 はグローバルな状態というより、コンポーネントの階層構造を生かした実装に向いてる、という記事
-
https://zenn.dev/yoshiko/articles/607ec0c9b0408d - 「3種類」で管理するReactのState戦略 by よしこ ↩
-
https://zenn.dev/neet/articles/f25abb616ec105 - React で h1-h6 を正しく使い分ける by Ryō Igarashi ↩