今までPages Routerだった処理サイトをApp Routerに移行してみたの記事にしてみた
PR
実装
App Routerに移行する場合は、以下を確認してディレクトレリを変更する必要がある
基本はsrc/pages/schedule/[id].tsx
→src/app/schedule/[id]/page.tsx
みたいな感じでディレクトレリを変更すればOK 👌
App RouterはReact Server Components(RSC)でレンダリングされているので対応、以下のようなコードになった
■ src/app/schedule/[id]/page.tsx
import { getServerAuthSession } from "~/server/auth"; import { redirect } from "next/navigation"; import { Template } from "~/app/_components/schedule/template"; import { api } from "~/trpc/server"; export default async function Page({ params }: { params: { id: string } }) { const session = await getServerAuthSession(); const url = await api.url.exists.query({ id: params.id }); if (url === false) { // 存在しないURLの場合はトップページに戻す redirect("/"); } const schedules = await api.schedule.fetch.query({ urlId: params.id }); return <Template login={!!session} schedules={schedules} id={params.id} />; }
■ src/app/_components/schedule/template.tsx
"use client"; import { signIn, signOut } from "next-auth/react"; import { useEffect, useCallback, useState } from "react"; import { useRouter } from "next/navigation"; import Period from "~/features/schedules/components/Period"; import Items from "~/features/schedules/components/Items"; import dayjs from "~/utils/dayjs"; import { monthItems, getScheduleInMonth } from "~/utils/schedule"; import Pagination from "~/features/schedules/components/Pagination"; import { toast } from "react-toastify"; import Layout from "~/components/Layout/Layout"; import { type Schedule } from "@prisma/client"; type Props = { login: boolean; id: string; schedules: Schedule[]; }; export function Template(props: Props) { const router = useRouter(); const [startDate, setStartDate] = useState(dayjs()); const [print, setPrint] = useState(false); const months = monthItems( Number(startDate.format("M")), Number(startDate.year()), ); const onLogout = useCallback(async () => { window.localStorage.setItem("URL_ID", ""); await signOut({ callbackUrl: "/", }); }, []); const onToIndex = useCallback(() => { window.localStorage.setItem("URL_ID", ""); router.push("/"); }, [router]); const onShare = useCallback(async () => { await global.navigator.clipboard.writeText(`${window.location.href}/share`); toast.success("URLをコピーしました!", { position: "top-center", autoClose: 2000, hideProgressBar: false, closeOnClick: true, pauseOnHover: true, draggable: true, progress: undefined, theme: "light", }); }, []); const onPrint = useCallback(() => { setPrint(true); setTimeout(() => window.print(), 100); }, []); useEffect(() => { const handleAfterPrint = () => { setPrint(false); }; window.addEventListener("afterprint", handleAfterPrint); return () => { window.removeEventListener("afterprint", handleAfterPrint); }; }, []); return ( <Layout> <> <main className="screen-container container mx-auto max-w-screen-xl gap-12 pt-3"> <div className="no-print"> <div className="absolute right-28 top-0 hidden sm:block"> <div className="flex w-14 cursor-pointer flex-col items-center pt-3 text-xl hover:bg-blue-100" onClick={() => void onShare()} > 🔗 <div className="text-xxs text-center text-gray-500">シェア</div> </div> </div> <div className="absolute right-14 top-0 hidden sm:block" onClick={() => void onPrint()} > <div className="flex w-14 cursor-pointer flex-col items-center pt-3 text-xl hover:bg-blue-100"> 🖨️ <div className="text-xxs text-center text-gray-500">印刷</div> </div> </div> <div className="absolute right-0 top-0 hidden sm:block" onClick={() => { if (props.login) { void onLogout(); } else { void signIn("credentials", { callbackUrl: "/", }); } }} > <div className="flex w-14 cursor-pointer flex-col items-center pt-3 text-xl hover:bg-blue-100"> {props.login ? "🔓" : "🗝️"} <div className="text-xxs text-center text-gray-500"> {props.login ? "ログアウト" : "ログイン"} </div> </div> </div> </div> <div className="relative hidden justify-between sm:flex"> <Period startDate={startDate.format()} endDate={startDate.add(1, "years").format()} /> <Pagination onNext={() => setStartDate(startDate.add(1, "months"))} onPrev={() => setStartDate(startDate.subtract(1, "months"))} /> </div> <div className="flex flex-col flex-nowrap justify-center pt-10 sm:flex-row sm:flex-wrap sm:justify-start"> {months.map((item, index) => ( <div key={index} className="item-container px-0 pb-6 sm:pb-16 sm:pr-16" > <Items urlId={props.id} date={item} defaultItems={getScheduleInMonth(item, props.schedules ?? [])} share={print} /> </div> ))} </div> <div className="no-print mb-10 flex justify-center"> {props.login ? ( <button type="button" className="mb-1 mr-1 rounded-md border border-red-700 px-5 py-2.5 text-center text-sm font-medium text-red-700 hover:bg-red-800 hover:text-white focus:outline-none focus:ring-2 focus:ring-red-300 dark:border-red-500 dark:text-red-500 dark:hover:bg-red-500 dark:hover:text-white dark:focus:ring-red-800" onClick={() => void onLogout()} > ログアウト </button> ) : ( <button type="button" className="mb-1 mr-1 rounded-md border border-gray-700 px-5 py-2.5 text-center text-sm font-medium text-blue-700 hover:bg-blue-800 hover:text-white focus:outline-none focus:ring-2 focus:ring-blue-300 dark:border-gray-500 dark:text-gray-500 dark:hover:bg-gray-500 dark:hover:text-white dark:focus:ring-gray-800" onClick={() => void onToIndex()} > トップページに戻る </button> )} </div> </main> </> </Layout> ); }
修正ポイントとしては以下の部分になる
RSCだとawaitが使用できるので以下のようなコードを書くことができる
export default async function Page({ params }: { params: { id: string } }) { const session = await getServerAuthSession(); const url = await api.url.exists.query({ id: params.id }); if (url === false) { // 存在しないURLの場合はトップページに戻す redirect("/"); } const schedules = await api.schedule.fetch.query({ urlId: params.id }); return <Template login={!!session} schedules={schedules} id={params.id} />;
useEffect
やuseState
に関してはRSCだと使用できないので、使用する場合はファイルの先頭でuse client
を宣言してClient Componentでimportする必要がある
"use client"; import { signIn, signOut } from "next-auth/react"; import { useEffect, useCallback, useState } from "react";
変更して見た感じの感想ですが、RSCにすることによって初回レンダリング時の認証やデータ取得のコードはシンプルになった、変更前と変更後のコードを比較すると以下の通り
実装内容はDBのUrlのtableにアクセスして該当のデータがなけばTOPページにリダイレクト & Urlのデータに認証情報が設定している場合に一致しない場合はTOPページにリダイレクト
■ 変更前
function Schedule() { const { data: sessionData } = useSession(); const router = useRouter(); const { id } = router.query; const url = api.url.exists.useQuery({ id: String(id) }); useEffect(() => { if (url.data === false) { // 存在しないURLの場合はトップページに戻す void router.push("/"); } if (url.data.userId != "" && url.data.userId != sessionData?.user?.id) { // userId設定のあるURLで認証情報が一致しない場合はトップページに戻す void router.push("/"); } }, [url.data, router, sessionData?.user?.id]);
■ 変更後
export default async function Page({ params }: { params: { id: string } }) { const session = await getServerAuthSession(); const url = await api.url.exists.query({ id: params.id }); if (url === null) { // 存在しないURLの場合はトップページに戻す redirect("/"); } if (url.userId != "" && url?.userId !== session?.user?.id) { // URLのユーザーIDとログインしているユーザーIDが一致しない場合はトップページに戻す redirect("/"); }
こんな感じでuseEffect
を使わなくても済むのでシンプルになるかなと思います。
逆にuseEffect
やuseState
を使いたい画面では必ずcomponentを分割してuse client
を宣言しないといけないのが手間だとも感じたので、今のところ凄い大きなメリットがある感じでも無いですが、App Routerじゃないと使用できない機能があったので、今回は移行してみました。