🥦

NextAuth.jsは何をしているのだ?

2023/10/09に公開
6

NextAuth.jsはとても便利らしいのだ。

なので試しに使ってみたのだ。
そしたら魔法みたいなコードだったのだ。すごかったのだ。

ただ、あまりにも魔法コードすぎて「中身はどうなってるのだ」と気になったので調べたのだ。

気になった部分

自分が気になったのは、おもに2つなのだ。

[...nextauth]/route.ts ←この書き方はなんなのだ?🤔

このよく分からない書き方は、Next.jsのdynamic routeという機能なのだ。

なので、NextAuth.jsとは無関係なのだ。

以下のスクショを見ると、イメージしやすいと思うのだ👇

つまり、この書き方をすることで「ここから先は全部NextAuthに任せるぅ!」ができるらしいのだ👇

[...nextauth]/route.ts
import NextAuth from "next-auth/next";
import { config } from "auth";

const handler = NextAuth(config);
export { handler as GET, handler as POST };

useSession() ←なんでこれでセッションが取れるのだ?🤔

これがこの記事を書くキッカケなのだ。

なんでコンポーネントの中でuseSession()って書くだけでセッションが取れるのだ?👇

export default function Header() {
  const { data: session } = useSession()
 ....
 

謎すぎるのだ。

というのもぼくの認識では、セッションというのは以下のようなものなのだ👇

  1. クライアント側が「僕のIDは1なのだ」なcookieをサーバーに投げる
    (もしくはhttpのauthorizationヘッダーにトークンをくっつけたりする)
  2. サーバー側は、DBとかに保存してるセッションのリストを見て「こやつがIDが1のやつなのだ」と特定する

この一連の「こやつなのだ」と特定する流れをセッションを呼ぶこともあるし、「セッションのリスト」のことをセッションと呼ぶこともある・・・という認識なのだ。

つまり、useSessionで取得できる「セッション」というのは「こやつなのだ」と特定するため情報を指すと思うのだ。だけどそれは本来サーバー側にあるものだと思うのだ。なのでuseSession()と書くだけでセッションの中身が取れるのは意味不明なのだ。

そんなわけでuseSession()の中身を見てみたのだ。
そしたら以下のような感じのコードが書いてあったのだ👇

  const value: SessionContextValue<R> = React.useContext(SessionContext)
  if (!value && process.env.NODE_ENV !== "production") {
    throw new Error(
      "[next-auth]: `useSession` must be wrapped in a <SessionProvider />"
    )
  }

まず「SessionContextの中身から、sessionを取ってくるのだ」な処理をしていたのだ。

なのでSessionContextを提供しているSessionProviderの中身を見てみたのだ👇

 ...
  const [session, setSession] = React.useState(() => {
    if (hasInitialSession) __NEXTAUTH._session = props.session
    return props.session
  })
  ...
  __NEXTAUTH._getSession = async ({ event } = {}) => {
      try {
        const storageEvent = event === "storage"
        if (storageEvent || __NEXTAUTH._session === undefined) {
          __NEXTAUTH._lastSync = now()
          __NEXTAUTH._session = await getSession({
            broadcast: !storageEvent,
          })
          setSession(__NEXTAUTH._session)
          return
        }
 ...

ざっと見た感じ「sessionというstateを用意して、getSessionという関数でsessionを取得する」みたいなことをしているのだ。

さらにgetSessionの中身を見てみると、以下のような感じになっていたのだ👇

export async function getSession(params?: GetSessionParams) {
  const session = await fetchData<Session>(
    "session",
    __NEXTAUTH,
    logger,
    params
  )
  ....
  return session
}

fetchDataというのは、fetch関数をラップしただけの関数だったのだ。

そんなわけで中身を読み解くと、getSession関数は

/sessionというパスにGETリクエストを投げるだけの関数

だったのだ。

ネットワークタブを開いてみると、たしかにページを開いただけで/sessionにアクセスされているのだ👇

魔法のようなコードの裏側は、じつは泥臭かったのだ。

開発者の人、ありがとうなのだ。

useSession()は効率が悪いのだ

useSession()の仕組みを知ってから思ったのだが、useSession()は効率が悪いのだ。

というのもNext13のApp Routerではすべてのコンポーネントがサーバーコンポーネントになったのだ。だからサーバーコンポーネントの場合は最初からサーバー側で「こやつなのだ」と特定してからコンポーネントを生成したほうが良いのだ。

useSession()を使うと無駄にfetchが走ってしまうのだ。だれも得しないのだ。
というかuseSession()はクライアント側の機能(Contextやstate)を使っているので、そもそもサーバーコンポーネントでは使えないのだ。

なので、サーバーコンポーネントの中でセッションを使いたい場合は、getServerSessionという関数を使うのだ。

以下みたいな感じなのだ👇

import { getServerSession } from "next-auth/next"
import { authOptions } from "pages/api/auth/[...nextauth]"

export default async function Page() {
  const session = await getServerSession(authOptions)
  return <pre>{JSON.stringify(session, null, 2)}</pre>
}

//引用:https://next-auth.js.org/configuration/nextjs

今後はこの書き方が主流になると思うのだ。

おまけ)NextAuth.js → Auth.js

名前が「Auth.js」に変わるらしいのだ。

Next.jsに限らず、いろいろなフレームワークや、素のJSにも対応するらしいのだ。

すごいのだ。これはとてもよいものなのだ。みんなも応援するのだ。

おわり

Discussion

tomo0611tomo0611

なのだっていうだけで、脳内でずんだもんボイスが聞こえてきますね!

place__ofplace__of

コードが読み切れていないのですが、<SessionProvider session={session}> みたいにしてサーバー側でgetServerSessionの値をSessionProviderに渡したら、__NEXTAUTH._sessionがundefinedではなくなりuseSession()してもサーバー側にfetch走らない雰囲気ありそうです
https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/react/index.tsx#L372

penpenpenpen

placeofさん、補足ありがとうなのだ。
たしかにその部分を見ると、そんな感じがするのだ。

でもコードを最後まで追うと、「最後に絶対getSessionするマン」になってるのだ👇

もともとのコメントは削除してるのだ
        if (storageEvent || __NEXTAUTH._session === undefined) { //👈SessionProviderでsessionを渡したらgetSessionは発火しないのだ!
          __NEXTAUTH._lastSync = now()
          __NEXTAUTH._session = await getSession({
            broadcast: !storageEvent,
          })
          setSession(__NEXTAUTH._session)
          return
        }

        if ( !event ||   __NEXTAUTH._session === null || //👈getSessionした結果、nullだった場合も発火しないのだ!
          now() < __NEXTAUTH._lastSync
        ) {
          return
        }

        __NEXTAUTH._lastSync = now()
        __NEXTAUTH._session = await getSession()//👈ここで必ずgetSessionすることになってるのだ!なぜなのだ!😣
        setSession(__NEXTAUTH._session)

コード引用元

僕はもうよく分からないのだ。

なのでApp Routerのドキュメントが整備されるまで待つことにするのだ😭

MSKMSK

私は今のところこうしてます。
sessionを取るラップ

ProviderWrap.tsx

'use client';
import { SessionProvider } from 'next-auth/react';

export default function ProviderWrap({
  children,
}: {
  children: React.ReactNode;
}) {
  return <SessionProvider>{children}</SessionProvider>;
}

layout.tsx

import './globals.css';
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import ProviderWrap from '@/app/contexts/ProviderWrap/index';

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="ja">
      <body className={inter.className}>
        <ProviderWrap>
          <main>{children}</main>
        </ProviderWrap>
      </body>
    </html>
  );
}

SignIn.tsx

'use client';
import Form from '@/app/components/layout/Form';
import { signIn, signOut, useSession } from 'next-auth/react';

export default function SignIn() {
  const { data: session } = useSession();

  if (session) {
    return (
      <>
        <Form user={session?.user} />
        <button
          type="button"
          onClick={() => signOut()}
          className="btn rounded-lg btn-base-100"
        >
          <span className="bg-clip-text hover:text-transparent bg-gradient-to-r from-pink-500 to-violet-500">
            Sign out
          </span>
        </button>
      </>
    );
  } else {
    return (
      <>
        <button
          type="button"
          onClick={() => signIn('google')}
          className="btn rounded-lg btn-base-100"
        >
          <p className="flex items-center justify-center">
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="24"
              height="24"
              fill="currentColor"
              className="bi bi-google mr-2"
              viewBox="0 0 16 16"
            >
              <path d="M15.545 6.558a9.42 9.42 0 0 1 .139 1.626c0 2.434-.87 4.492-2.384 5.885h.002C11.978 15.292 10.158 16 8 16A8 8 0 1 1 8 0a7.689 7.689 0 0 1 5.352 2.082l-2.284 2.284A4.347 4.347 0 0 0 8 3.166c-2.087 0-3.86 1.408-4.492 3.304a4.792 4.792 0 0 0 0 3.063h.003c.635 1.893 2.405 3.301 4.492 3.301 1.078 0 2.004-.276 2.722-.764h-.003a3.702 3.702 0 0 0 1.599-2.431H8v-3.08h7.545z" />
            </svg>
            <span className="bg-clip-text hover:text-transparent bg-gradient-to-r from-pink-500 to-violet-500">
              Sign in with Google
            </span>
          </p>
        </button>
      </>
    );
  }
}

takecchitakecchi

useSession()は効率が悪いのだ

用途が違う中、サーバーサイドの処理と比較して"効率が悪い"とするのは不適切かなと。

文中にもありますが、クライアント向けに提供されている機能なので、httpOnlyなcookieの有効性をfetchで確認していること自体に違和感は感じませんでした。

"useSession()は効率が悪い"という見出しだと読み手に誤解を与えそうだなと思ったのですみません...🙇‍♂️