はじめに
今回は、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
からHeader
、Header
からNavbar
にprops
でuser
をバケツリレーしています。
また、Home
からSidebar
、Sidebar
からSidebarList
にもuser
をバケツリレーしています。
※ 今回はわかりやすくuserを多用し、バケツリレーするようにしましたが、ここまで使われているものは本来はグローバルに管理したほうが良いです
上記はまだマシですが、コンポーネントが増え、onClick
やtitle
、color
、disabled
など渡すべきprops
も増えると、バケツリレー問題が深刻になり、苦しむことになります。
propsにJSXを渡そう
これまで見てきたように、コンポーネントが増え規模が大きくなるとバケツリレー問題が起きやすく、props
やState
の管理に苦しむことになります。
そこで、この問題を簡単に解消する方法となるのが、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
Header
のprops
に直接、JSX.Element
としてNavbar
を渡すことでuser
をHome
から直接Navbar
に渡すことができます。
これを引き出しとして持っておけば、以下のようなこともできます。
例えば、Home
→Navbar
→Navbar
→NavbarItem
にuser
を渡す場合でも、Navbar
にJSX.Element
のprops
を渡し、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
なので、そこも個人的には嬉しい点ではありました。