🦁

【NextJs14】NextJs14 と 便利なライブラリ【#12Zustand 】

2023/12/25に公開

【#12Zustand 】

YouTube: https://youtu.be/JFTHJqfs5eE

https://youtu.be/JFTHJqfs5eE

今回は「Zustand」を使用して
モーダルの状態をストアで管理します。

https://docs.pmnd.rs/zustand/getting-started/introduction

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