🦁
【NextJs14】NextJs14 と 便利なライブラリ【#12Zustand 】
【#12Zustand 】
YouTube: https://youtu.be/JFTHJqfs5eE
今回は「Zustand」を使用して
モーダルの状態をストアで管理します。
npm install zustand
ルート直下に「store」フォルダを作成して、
以下のファイルを作成します。
store/use-modal-store.ts
import { create } from "zustand";
type UseModalStore = {
isOpen: boolean;
onOpen: () => void;
onClose: () => void;
};
export const useModal = create<UseModalStore>((set) => ({
isOpen: false,
onOpen: () => set({ isOpen: true }),
onClose: () => set({ isOpen: false }),
}));
上記で作成した値と関数をモーダルのコンポーネントに設定します。
components/modals/user-profile-modal.tsx
"use client";
import { Copy } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useModal } from "@/store/use-modal-store";
interface UserProfileModalProps {
email: string;
}
export function UserProfileModal({ email }: UserProfileModalProps) {
const { isOpen, onClose } = useModal();
const onClickCopy = () => {
navigator.clipboard.writeText(email);
toast.success("Copy Succeed!");
onClose();
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Share link</DialogTitle>
<DialogDescription>
Anyone who has this link will be able to view this.
</DialogDescription>
</DialogHeader>
<div className="flex items-center space-x-2">
<div className="grid flex-1 gap-2">
<Label htmlFor="link" className="sr-only">
Link
</Label>
<Input id="link" defaultValue={email} readOnly />
</div>
<Button
onClick={onClickCopy}
type="button"
size="sm"
className="px-3"
>
<span className="sr-only">Copy</span>
<Copy className="h-4 w-4" />
</Button>
</div>
<DialogFooter className="sm:justify-start">
<Button onClick={onClose} type="button" variant="secondary">
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
モーダルをページからではなくプロバイダーを使用して、
一番上のlayout.tsxから開けるように設定します。
components/providers/modals-provider.tsx
"use client";
import { useEffect, useState } from "react";
import { useUser } from "@clerk/nextjs";
import { UserProfileModal } from "../modals/user-profile-modal";
export const ModalsProvider = () => {
const [isMounted, setIsMounted] = useState(false);
const { user } = useUser();
useEffect(() => {
setIsMounted(true);
}, []);
if (!isMounted) {
return null;
}
return (
<>
<UserProfileModal email={user?.emailAddresses[0].emailAddress || ""} />
</>
);
};
プロバイダーをlayout.tsxに設定します。
app/layout.tsx
import type { Metadata } from "next";
import { ClerkProvider } from "@clerk/nextjs";
import { Inter } from "next/font/google";
import { Toaster } from "sonner";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
import { ModalsProvider } from "@/components/providers/modals-provider";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" suppressHydrationWarning>
<ClerkProvider>
<body className={`${inter.className} bg-white dark:bg-slate-800`}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<ModalsProvider />
<Toaster />
{children}
</ThemeProvider>
</body>
</ClerkProvider>
</html>
);
}
モーダルを開くボタンをサイドバーに設定します。
app/(main)/components/main-sidebar.tsx
"use client";
import { Copy, Database, Home, Image, ShieldCheck, User } from "lucide-react";
import { Separator } from "@/components/ui/separator";
import { MainSidebarItem } from "./main-sidebar-item";
import { Button } from "@/components/ui/button";
import { useModal } from "@/store/use-modal-store";
export type MenuItem = {
id: string;
href: string;
pathname: string;
label: string;
icon: React.ReactNode;
};
const menuItems = [
{
id: "0",
href: "/home",
pathname: "/home",
label: "Home",
icon: <Home className="h-5 w-5 mr-3 group-hover:text-white" />,
},
{
id: "1",
href: "/account",
pathname: "/account",
label: "Account",
icon: <User className="h-5 w-5 mr-3 group-hover:text-white" />,
},
{
id: "2",
href: "/images",
pathname: "/images",
label: "Images",
icon: <Image className="h-5 w-5 mr-3 group-hover:text-white" />,
},
{
id: "3",
href: "/data",
pathname: "/data",
label: "Data",
icon: <Database className="h-5 w-5 mr-3 group-hover:text-white" />,
},
{
id: "4",
href: "/protected",
pathname: "/protected",
label: "Protected",
icon: <ShieldCheck className="h-5 w-5 mr-3 group-hover:text-white" />,
},
];
export const MainSidebar = () => {
const { onOpen } = useModal();
return (
<div className="w-full flex py-4 px-4 flex-col items-center">
<div className="w-full">
<h3 className="text-lg">Menu</h3>
</div>
<Separator className="bg-slate-400 mt-2" />
<div className="w-full py-4">
<ul>
{menuItems.map((item) => (
<MainSidebarItem key={item.id} item={item} />
))}
</ul>
</div>
<Separator className="bg-slate-400 mt-2" />
<div className="w-full py-4">
<Button
onClick={onOpen}
className="w-full flex items-center gap-x-2"
variant="primary"
>
<Copy className="h-4 w-4" />
Copy Email
</Button>
</div>
</div>
);
};
Discussion