11
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-01-15

はじめに

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

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

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

実装

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

コードの全体像は以下です。

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からHeaderHeaderからNavbarpropsuserをバケツリレーしています。
また、HomeからSidebarSidebarからSidebarListにもuserをバケツリレーしています。
※ 今回はわかりやすくuserを多用し、バケツリレーするようにしましたが、ここまで使われているものは本来はグローバルに管理したほうが良いです

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

propsにJSXを渡そう

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

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

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

Headerpropsに直接、JSX.ElementとしてNavbarを渡すことでuserHomeから直接Navbarに渡すことができます。

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

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なので、そこも個人的には嬉しい点ではありました。

11
4
0

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
11
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?