【Next.js・React】propsのバケツリレーで苦しまない方法

# はじめに
今回は、Reactでよくある`props`のバケツリレーについて書いていこうと思います。

Reactで開発しているとコンポーネントが増え`props`のバケツリレーがいたるところで発生するかと思います。
そして、バケツリレーが深くなり、可読性が落ちた段階で、「状態管理ライブラリを使おうか」「`ContextAPI`を使おうか」と考えるのはある程度の人が経験したことだと思います。

ただ、「`ContextAPI`は初学者にとっては敷居が高く、状態管理ライブラリも学習コスト0とはいかない、`props`のみで解決したい」という場面で有効な手段になり得る解決法を紹介します。

# 実装
今回はよくあるトップページのUIを`v0`に作ってもらい、それを編集したものを使用します。

https://v0.dev/

コードの全体像は以下です。
```tsx
import Link from 'next/link'

import {
  BarChartIcon,
  FacebookIcon,
  InstagramIcon,
  MountainIcon,
  PackageIcon,
  SettingsIcon,
  TwitterIcon,
  UsersIcon,
} from '../components/icons'

type User = Record<'name' | 'email', string>

const Navbar = (props: { user: User }) => {
  return (
    <nav className="ml-auto flex gap-4">
      <Link
        className="text-sm font-medium underline-offset-4 hover:underline"
        href="#"
      >
        About
      </Link>
      <Link
        className="text-sm font-medium underline-offset-4 hover:underline"
        href="#"
      >
        Pricing
      </Link>
      <Link
        className="text-sm font-medium underline-offset-4 hover:underline"
        href="#"
      >
        Contact
      </Link>
      <Link
        className="text-sm font-medium underline-offset-4 hover:underline"
        href="#"
      >
        {props.user ? 'SignOut' : 'SignIn'}
      </Link>
    </nav>
  )
}

const Header = (props: { user: User }) => {
  return (
    <header className="flex h-16 items-center border-b px-4 md:px-6">
      <Link className="flex items-center gap-2" href="#">
        <MountainIcon className="h-6 w-6" />
        <span className="text-sm font-semibold">Acme Inc</span>
      </Link>
      <Navbar user={props.user} />
    </header>
  )
}

const Sidebar = (props: { user: User }) => {
  return (
    <aside className="w-64 border-r p-4">
      <h2 className="mb-4 text-lg font-bold">Menu</h2>
      <SidebarList user={props.user} />
    </aside>
  )
}

const SidebarList = (props: { user: User }) => {
  return (
    <nav className="space-y-2">
      <SidebarItem
        title="Products"
        icon={<PackageIcon className="h-4 w-4" />}
      />
      <SidebarItem
        title={props.user.name}
        icon={<UsersIcon className="h-4 w-4" />}
      />
      <SidebarItem
        title="Analytics"
        icon={<BarChartIcon className="h-4 w-4" />}
      />
      <SidebarItem
        title="Settings"
        icon={<SettingsIcon className="h-4 w-4" />}
      />
    </nav>
  )
}

const SidebarItem = (props: { title: string; icon: JSX.Element }) => {
  return (
    <button className="flex items-center gap-2 text-sm font-medium">
      {props.icon}
      {props.title}
    </button>
  )
}

const Content = (props: { user: User }) => {
  return (
    <main className="flex-1 p-4">
      <h1 className="mb-4 text-2xl font-bold">Welcome to {props.user.name}</h1>
      <p className="text-gray-500 dark:text-gray-400">
        Manage your products, customers and analytics with ease.
      </p>
    </main>
  )
}

const Footer = () => {
  return (
    <footer className="flex h-16 items-center border-t px-4 md:px-6">
      <nav className="flex gap-4">
        <Link href="#">
          <FacebookIcon className="h-6 w-6" />
        </Link>
        <Link href="#">
          <TwitterIcon className="h-6 w-6" />
        </Link>
        <Link href="#">
          <InstagramIcon className="h-6 w-6" />
        </Link>
      </nav>
      <p className="ml-auto text-xs text-gray-500 dark:text-gray-400">
        © 2024 Acme Inc. All rights reserved.
      </p>
    </footer>
  )
}

const Home = () => {
  const user = { name: 'foo', email: 'foo@example.com' }

  return (
    <div className="flex min-h-screen flex-col">
      <Header user={user} />
      <div className="flex flex-1 overflow-hidden">
        <Sidebar user={user} />
        <Content user={user} />
      </div>
      <Footer />
    </div>
  )
}

export default Home
```

よくある構成ですが、`Home`から`Header`、`Header`から`Navbar`に`props`で`user`をバケツリレーしています。
また、`Home`から`Sidebar`、`Sidebar`から`SidebarList`にも`user`をバケツリレーしています。
※ 今回はわかりやすくuserを多用し、バケツリレーするようにしましたが、ここまで使われているものは本来はグローバルに管理したほうが良いです

上記はまだマシですが、コンポーネントが増え、`onClick`や`title`、`color`、`disabled`など渡すべき`props`も増えると、バケツリレー問題が深刻になり、苦しむことになります。

# propsにJSXを渡そう
これまで見てきたように、コンポーネントが増え規模が大きくなるとバケツリレー問題が起きやすく、`props`や`State`の管理に苦しむことになります。
そこで、この問題を簡単に解消する方法となるのが、`props`にコンポーネントを渡すというものです。

以下は、先程のコードを改善したものです。

```tsx
import Link from 'next/link'

import {
  BarChartIcon,
  FacebookIcon,
  InstagramIcon,
  MountainIcon,
  PackageIcon,
  SettingsIcon,
  TwitterIcon,
  UsersIcon,
} from '@/components/icons'

type User = Record<'name' | 'email', string>

const Navbar = (props: { user: User }) => {
  return (
    <nav className="ml-auto flex gap-4">
      <Link
        className="text-sm font-medium underline-offset-4 hover:underline"
        href="#"
      >
        About
      </Link>
      <Link
        className="text-sm font-medium underline-offset-4 hover:underline"
        href="#"
      >
        Pricing
      </Link>
      <Link
        className="text-sm font-medium underline-offset-4 hover:underline"
        href="#"
      >
        Contact
      </Link>
      <Link
        className="text-sm font-medium underline-offset-4 hover:underline"
        href="#"
      >
        {props.user ? 'SignOut' : 'SignIn'}
      </Link>
    </nav>
  )
}

const Header = (props: { navbar: JSX.Element }) => {
  return (
    <header className="flex h-16 items-center border-b px-4 md:px-6">
      <Link className="flex items-center gap-2" href="#">
        <MountainIcon className="h-6 w-6" />
        <span className="text-sm font-semibold">Acme Inc</span>
      </Link>
      {props.navbar}
    </header>
  )
}

const Sidebar = (props: { list: JSX.Element }) => {
  return (
    <aside className="w-64 border-r p-4">
      <h2 className="mb-4 text-lg font-bold">Menu</h2>
      {props.list}
    </aside>
  )
}

const SidebarList = (props: { user: User }) => {
  return (
    <nav className="space-y-2">
      <SidebarItem
        title="Products"
        icon={<PackageIcon className="h-4 w-4" />}
      />
      <SidebarItem
        title={props.user.name}
        icon={<UsersIcon className="h-4 w-4" />}
      />
      <SidebarItem
        title="Analytics"
        icon={<BarChartIcon className="h-4 w-4" />}
      />
      <SidebarItem
        title="Settings"
        icon={<SettingsIcon className="h-4 w-4" />}
      />
    </nav>
  )
}

const SidebarItem = (props: { title: string; icon: JSX.Element }) => {
  return (
    <button className="flex items-center gap-2 text-sm font-medium">
      {props.icon}
      {props.title}
    </button>
  )
}

const Content = (props: { user: User }) => {
  return (
    <main className="flex-1 p-4">
      <h1 className="mb-4 text-2xl font-bold">Welcome to {props.user.name}</h1>
      <p className="text-gray-500 dark:text-gray-400">
        Manage your products, customers and analytics with ease.
      </p>
    </main>
  )
}

const Footer = () => {
  return (
    <footer className="flex h-16 items-center border-t px-4 md:px-6">
      <nav className="flex gap-4">
        <Link href="#">
          <FacebookIcon className="h-6 w-6" />
        </Link>
        <Link href="#">
          <TwitterIcon className="h-6 w-6" />
        </Link>
        <Link href="#">
          <InstagramIcon className="h-6 w-6" />
        </Link>
      </nav>
      <p className="ml-auto text-xs text-gray-500 dark:text-gray-400">
        © 2024 Acme Inc. All rights reserved.
      </p>
    </footer>
  )
}

const Home = () => {
  const user = { name: 'foo', email: 'foo@example.com' }

  return (
    <div className="flex min-h-screen flex-col">
      <Header navbar={<Navbar user={user} />} />
      <div className="flex flex-1 overflow-hidden">
        <Sidebar list={<SidebarList user={user} />} />
        <Content user={user} />
      </div>
      <Footer />
    </div>
  )
}

export default Home

```

`Header`の`props`に直接、`JSX.Element`として`Navbar`を渡すことで`user`を`Home`から直接`Navbar`に渡すことができます。

これを引き出しとして持っておけば、以下のようなこともできます。
例えば、`Home`→`Navbar`→`Navbar`→`NavbarItem`に`user`を渡す場合でも、`Navbar`に`JSX.Element`の`props`を渡し、`Home`から`NavbarItem`に直接、｀user`を渡すことができます。

```tsx
const NavbarItem = (props: { user: User }) => {
  return (
     <Link
        className="text-sm font-medium underline-offset-4 hover:underline"
        href="#"
      >
        {props.user ? 'SignOut' : 'SignIn'}
    </Link>
  )
}
const Navbar = (props: { item: JSX.Element }) => {
  return (
    <nav className="ml-auto flex gap-4">
     {item}
    </nav>
  )
}

const Home = () => {
  const user = { name: 'foo', email: 'foo@example.com' }

  return (
    <div className="flex min-h-screen flex-col">
      <Header navbar={<Navbar item={<NavbarItem user={props.user} />} />} />
      <div className="flex flex-1 overflow-hidden">
        <Sidebar list={<SidebarList user={user} />} />
        <Content user={user} />
      </div>
      <Footer />
    </div>
  )
}
```

このようにすることで、親となるコンポーネントで情報を一括管理できたり、コンポーネントを切り分けてリファクタリングしやすくなり、可読性を高めることができます。

ただ、グローバルに管理すべきデータももちろんあるので、便利ではあるのですが、何でもかんでも`JSX.Element`で渡せばよいということではないということは念頭に置いておきましょう。
ケース・バイ・ケースで要件に合わせて使い分けられることが重要です。

# おわりに
感想ですが、`v0`でのUI作成が精度が高く、驚きました。
生成されたコードも`tailwindCSS`なので、そこも個人的には嬉しい点ではありました。