Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useSession only getting the session after manually reloading the page #9504

Open
getr62 opened this issue Dec 30, 2023 · 30 comments
Open

useSession only getting the session after manually reloading the page #9504

getr62 opened this issue Dec 30, 2023 · 30 comments
Labels
bug Something isn't working triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime.

Comments

@getr62
Copy link

getr62 commented Dec 30, 2023

Environment

System:
OS: Linux 5.15 Ubuntu 22.04.3 LTS 22.04.3 LTS (Jammy Jellyfish)
CPU: (6) x64 Common KVM processor
Memory: 48.93 GB / 62.79 GB
Container: Yes
Shell: 5.1.16 - /bin/bash
Binaries:
Node: 20.10.0 - ~/.nvm/versions/node/v20.10.0/bin/node
npm: 10.2.3 - ~/.nvm/versions/node/v20.10.0/bin/npm
npmPackages:
@auth/core: 0.18.1 => 0.18.1
next: 14.0.2 => 14.0.2
next-auth: ^5.0.0-beta.4 => 5.0.0-beta.4
react: ^18 => 18.2.0
Browser:
Chrome
Fireox

Reproduction URL

https://github.com/getr62/discuss

Describe the issue

I am using the credential provider, because I have to develop an app which is only available in a local network, maybe later also running on computers even without connection to the internet, so OAuth etc is no option for me.

src/lib/auth.ts

import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { z } from 'zod';
import { db } from '@/db';
import type { User } from '@prisma/client';

async function getUser(name: string): Promise<User | null> {
  try {
    const user = await db.user.findFirst({
      where: {
        name,
      },
    });
    return user;
  } catch (error) {
    throw new Error('Failed to fetch user.');
  }
}

export const {
  handlers: { GET, POST },
  auth,
  signOut,
  signIn,
} = NextAuth({
  providers: [
    CredentialsProvider({
      name: 'Credentials',
      async authorize(credentials) {
        const parsedCredentials = z
          .object({
            username: z.string(),
            password: z.string(),
          })
          .safeParse(credentials);

        // console.log('credentials: ', credentials);
        if (parsedCredentials.success) {
          const { username, password } = parsedCredentials.data;
          const user = await getUser(username);
          console.log('USER in auth.ts AUTHORIZE: ', user);
          if (!user) return null;
          return user;
        }

        return null;
      },
    }),
  ],
  session: {
    strategy: 'jwt',
  },
  secret: process.env.AUTH_SECRET,
  callbacks: {
    async jwt({ token, user }: any) {
      console.log('USER in JWT callback: ', user);
      console.log('TOKEN BEFORE modification in JWT callback: ', token);
      if (token && user) {
        token.role = user.role;
        token.id = user.id;
      }
      console.log('TOKEN AFTER modification in JWT callback: ', token);
      return token;
    },
    async session({ session, token, user }: any) {
      console.log('USER in SESSION callback: ', user);
      console.log('TOKEN BEFORE modification in SESSION callback: ', token);
      console.log('SESSION BEFORE modification in SESSION callback: ', session);
      if (session && token) {
        session.user.id = token.id;
        session.user.role = token.role;

        console.log('SESSION AFTER modification in SESSION callback: ', session);
      }

      return session;
    },
  },
  pages: {
    signIn: '/src/components/auth/sign-in-form',
  },
});

In Next.js I am using the app router with server actions, also for the sign-in functionality.

src/actions/sign-in.tx

'use server';

import type { Profile } from '@prisma/client';
import { z } from 'zod';
import { db } from '@/db';
import { redirect } from 'next/navigation';
import paths from '@/lib/paths';
import * as auth from '@/lib/auth';
import { compare } from 'bcrypt';

const signInSchema = z.object({
  username: z.string().min(3),
  password: z.string().min(7),
});

interface SignInFormState {
  errors: {
    username?: string[];
    password?: string[];
    _form?: string[];
  };
}

export async function signIn(
  formState: SignInFormState,
  formData: FormData
): Promise<SignInFormState> {
  const result = signInSchema.safeParse({
    username: formData.get('username'),
    password: formData.get('password'),
  });

  console.log('result signInSchema safeParse: ', result);

  if (!result.success) {
    return {
      errors: result.error.flatten().fieldErrors,
    };
  }

  const user = await db.user.findUnique({
    where: {
      name: result.data.username,
    },
  });

  if (!user) {
    return {
      errors: {
        _form: ['Wrong credentials'],
      },
    };
  }

  const passwordsMatch = await compare(result.data.password, user.password);

  if (!passwordsMatch) {
    return {
      errors: {
        _form: ['Wrong credentials'],
      },
    };
  }

  console.log('user found in sign-in action: ', user);

  let profile: Profile;
  try {
    await auth.signIn('credentials', {
      redirect: false,
      username: result.data.username,
      password: result.data.password,
    });

    profile = await db.profile.upsert({
      where: {
        userId: user.id,
      },
      update: {
        lastLogin: new Date().toLocaleString(),
      },
      create: {
        userId: user.id,
        lastLogin: new Date().toLocaleString(),
      },
    });
  } catch (err: unknown) {
    if (err instanceof Error) {
      console.log('error in auth.signIn action: ', err);
      return {
        errors: {
          _form: [err.message],
        },
      };
    } else {
      return {
        errors: {
          _form: ['Something went wrong'],
        },
      };
    }
  }

  redirect(paths.home());
  return {
    errors: {},
  };
}

The logged in user should be displayed in the header component. The header is part of the root-layout file. To avoid that in the build process every page is treated as a dynamic route, this on the authentication state depending part is put in a client component.

static build home page

src/components/header

import Link from 'next/link';
import { Suspense } from 'react';
import {
  Navbar,
  NavbarBrand,
  NavbarContent,
  NavbarItem,
} from '@nextui-org/react';
import HeaderAuth from '@/components/header-auth';
import SearchInput from '@/components/search-input';
import dynamic from 'next/dynamic';

const DynamicHeaderAuth = dynamic(() => import('./header-auth'), {
  ssr: false,
});

export default function Header() {
  return (
    <Navbar className='shadow mb-6'>
      <NavbarBrand>
        <Link href='/' className='font-bold'>
          Discuss
        </Link>
      </NavbarBrand>
      <NavbarContent justify='center'>
        <NavbarItem>
          <Suspense>
            <SearchInput />
          </Suspense>
        </NavbarItem>
      </NavbarContent>

      <NavbarContent justify='end'>
        {/* <HeaderAuth /> */}
        <DynamicHeaderAuth />
      </NavbarContent>
    </Navbar>
  );
}

src/components/header-auth

'use client';

import Link from 'next/link';
import {
  Chip,
  NavbarItem,
  Button,
  Popover,
  PopoverTrigger,
  PopoverContent,
} from '@nextui-org/react';
import { Icon } from 'react-icons-kit';
import { pacman } from 'react-icons-kit/icomoon/pacman';
import { useSession } from 'next-auth/react';
import * as actions from '@/actions';

export default function HeaderAuth() {
  const session = useSession();
  console.log('session 1 from useSession in header-auth: ', session);

  let authContent: React.ReactNode;
  if (session.status === 'loading') {
    authContent = null;
  } else if (session.data?.user) {
    console.log('session 2 from useSession in header-auth: ', session);
    authContent = (
      <Popover placement='left'>
        <PopoverTrigger>
          <Chip
            className='cursor-pointer'
            startContent={<Icon icon={pacman} />}
            variant='faded'
            color='default'
          >
            {session.data.user.name}
          </Chip>
        </PopoverTrigger>
        <PopoverContent>
          <div className='p-4'>
            <form action={actions.signOut}>
              <Button type='submit'>Sign Out</Button>
            </form>
          </div>
        </PopoverContent>
      </Popover>
    );
  } else {
    authContent = (
      <>
        <NavbarItem>
          <Link href={'/sign-in'}>Sign In</Link>
        </NavbarItem>

        <NavbarItem>
          <Link href={'/sign-up'}>Sign Up</Link>
        </NavbarItem>
      </>
    );
  }

  return authContent;
}

As you can see in the auth.ts file I am heavily logging different stages in the jwt and session. Basically I am able to prep the token with the user id and role as well as all these values into the session with just one big problem. The session is only created, after I manually reload the page. To showcase I uploaded a video where you can see in the browser console that the session state only changes from unauthenticated to authenticated when the page is manually reloaded.

sign-in-short.mp4

Sure, I am by far no experienced developer, but I read in the discussions that some other people have issues too using the useSession hook and not getting the session back from the server.

How to reproduce

I linked a small example project, where I was experimenting with these features like server actions. In there should be everything to run the app with the exception of the .env file which contains these values:

DATABASE_URL="postgresql://user:password@localhost:5434/discuss_ext?schema=public"
AUTH_SECRET=************
NEXTAUTH_URL=http://localhost:3004

As you can see, nextjs runs on port 3004 which is hard coded in project.json and the database port is set to port 5434, which is also used in the docker-compose.yml

Expected behavior

After signing into the app the authentication state should change automatically from unauthenticated to authenticated.

@getr62 getr62 added bug Something isn't working triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime. labels Dec 30, 2023
@vinayavodah

This comment was marked as off-topic.

@vinayavodah
Copy link

@getr62 Were you able to get around this?

@getr62
Copy link
Author

getr62 commented Jan 10, 2024

@vinayavodah unfortunately not yet

@luckykenlin

This comment was marked as off-topic.

@getr62
Copy link
Author

getr62 commented Jan 18, 2024

I pushed the new branch 'common-reload' to github which solved the problem, at least for now. In the readme is a short description how I implemented this work around.
If you are interested, I will try tomorrow to write a bit more, what I exactly did.
But please be aware that I am a noob coder and I am sure the developers of authjs will probably regard my solution as awful ;)

@luckykenlin
Copy link

luckykenlin commented Jan 18, 2024

I pushed the new branch 'common-reload' to github which solved the problem, at least for now. In the readme is a short description how I implemented this work around. If you are interested, I will try tomorrow to write a bit more, what I exactly did. But please be aware that I am a noob coder and I am sure the developers of authjs will probably regard my solution as awful ;)

Yup, I did the same way, but it still needs to hard reload to refresh the session which is stored in the Session Context.

The update function from useSession won't work as expected.

@bocarw121
Copy link

bocarw121 commented Jan 19, 2024

Hey,

Yea that's strange that the useSession hook is not getting the session after you sign in, but what you can do to avoid that custom loader is the following:

Since your Header component is a server component you can turn it into an async function and you can get the session directly in the component by awaiting the auth function from ./src/lib/auth.ts like this const session = await auth(); and passing the session as props to your <DynamicHeaderAuth session={session} /> component

./src/components/header.tsx

import Link from "next/link";
import { Suspense } from "react";
import {
  Navbar,
  NavbarBrand,
  NavbarContent,
  NavbarItem,
} from "@nextui-org/react";
import HeaderAuth from "@/components/header-auth";
import SearchInput from "@/components/search-input";
import dynamic from "next/dynamic";
import { auth } from "@/lib/auth";

const DynamicHeaderAuth = dynamic(() => import("./header-auth"), {
  ssr: false,
});

export default async function Header() {
  const session = await auth();
  return (
    <Navbar className="shadow mb-6">
      <NavbarBrand>
        <Link href="/" className="font-bold">
          Discuss
        </Link>
      </NavbarBrand>
      <NavbarContent justify="center">
        <NavbarItem>
          <Suspense>
            <SearchInput />
          </Suspense>
        </NavbarItem>
      </NavbarContent>

      <NavbarContent justify="end">
        {/* <HeaderAuth /> */}
        <DynamicHeaderAuth session={session} />
      </NavbarContent>
    </Navbar>
  );
}

and in the HeaderAuth you can do this

./src/components/header-auth.tsx

"use client";

import Link from "next/link";
import {
  Chip,
  NavbarItem,
  Button,
  Popover,
  PopoverTrigger,
  PopoverContent,
} from "@nextui-org/react";
import { Icon } from "react-icons-kit";
import { pacman } from "react-icons-kit/icomoon/pacman";
import { useSession } from "next-auth/react";
import * as actions from "@/actions";
import { Session } from "next-auth";
import { useEffect } from "react";

export default function HeaderAuth({ session }: { session: Session | null }) {
  if (session?.user) {
    console.log("session 2 from useSession in header-auth: ", session);
    return (
      <Popover placement="left">
        <PopoverTrigger>
          <Chip
            className="cursor-pointer"
            startContent={<Icon icon={pacman} />}
            variant="faded"
            color="default"
          >
            {session.user.name}
          </Chip>
        </PopoverTrigger>
        <PopoverContent>
          <div className="p-4">
            <form action={actions.signOut}>
              <Button type="submit">Sign Out</Button>
            </form>
          </div>
        </PopoverContent>
      </Popover>
    );
  }

  return (
    <>
      <NavbarItem>
        <Link href={"/sign-in"}>Sign In</Link>
      </NavbarItem>

      <NavbarItem>
        <Link href={"/sign-up"}>Sign Up</Link>
      </NavbarItem>
    </>
  );
}

and you should be good to go as far as your header goes.

as a side note make sure to always pass the session to SessionProvider in your layout like this:

./page/layout.tsx

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Providers from "@/app/providers";
import Header from "@/components/header";
import { auth } from "@/lib/auth";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Discuss",
  description: "",
};

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const session = await auth();
  return (
    <html lang="en">
      <body className={inter.className}>
        <div className="container mx-auto px-4 max-w-6xl">
          <Providers session={session}>
            <Header />
            {children}
          </Providers>
        </div>
      </body>
    </html>
  );
}

and in your Providers you can do this

./app/providers.tsx

"use client";

import { NextUIProvider } from "@nextui-org/react";
import { Session } from "next-auth";
import { SessionProvider } from "next-auth/react";

interface ProvidersProps {
  children: React.ReactNode;
  session?: Session | null;
}

export default function Providers({ children, session }: ProvidersProps) {
  return (
    <SessionProvider session={session}>
      <NextUIProvider>{children}</NextUIProvider>
    </SessionProvider>
  );
}

I haven't had this issue, but if I find something out I will keep you posted. Hope this helps for now.

You can also refer to the v5 upgrade guide. Keep in mind that it is still in beta and the API will be changing. Take care!

@luckykenlin
Copy link

hi, @bocarw121

Thanks for your help. for my application, I heavily use the useSession hook everywhere, It is not easy to pass the session from sever everywhere.

The update function https://next-auth.js.org/getting-started/client#updating-the-session only works when the session is authenticated.

any possible to refresh the session by triggering useSession.refresh or something

@bocarw121
Copy link

bocarw121 commented Jan 19, 2024

hi, @luckykenlin

Yea, there is no refresh or anything like that but i found something else.

There is the getSession helper that calls /api/auth/session to retrieve the current active session. You can call the getSession helper inside of a hook and return the session and status and use the hook in your client components.

./hooks/useCurrentSession

import { Session } from "next-auth";
import { getSession } from "next-auth/react";
import { usePathname } from "next/navigation";
import { useState, useEffect, useCallback } from "react";

// This hook doesn't rely on the session provider
export const useCurrentSession = () => {
  const [session, setSession] = useState<Session | null>(null);
  const [status, setStatus] = useState<string>("unauthenticated");
  const pathName = usePathname();

  const retrieveSession = useCallback(async () => {
    try {
      setStatus("loading");
      const sessionData = await getSession();

      if (sessionData) {
        setSession(sessionData);
        setStatus("authenticated");
        return;
      }

      setStatus("unauthenticated");
    } catch (error) {
      setStatus("unauthenticated");
      setSession(null);
    }
  }, []);

  useEffect(() => {
    retrieveSession();

    // use the pathname to force a re-render when the user navigates to a new page
  }, [retrieveSession, pathName]);

  return { session, status };
};

./components/MyComponent

"use client";
import { useCurrentSession } from "@/hooks/useCurrentSession";

type MyComponentProps = {};

export const MyComponent = (props: MyComponentProps) => {
  const { session, status } = useCurrentSession();

  if (status === "loading") {
    return <h1>Loading...</h1>;
  }

  if (status === "unauthenticated") {
    return (
      <>
        <h1>Unauthenticated</h1>
        <p>Please sign in to continue.</p>
      </>
    );
  }

  return <h1>Welcome {session?.user?.name}</h1>;
};

You can also tweak the hook to fit your needs, hope this helps.

@milindgoel15
Copy link

The problem lies in your code itself. To use the useSession hook, it must know if the session is available or not. So when declaring the session provider context in the layout, the session has to be fetched in the layout and passed on to the sessionprovider to be able to make use of useSession client hook.

Without passing the session, the hook doesn't know if the session is available or not.

The fix is to provide the session through props right in your provider as also provided by @bocarw121 :

./app/providers.tsx

"use client";

import { NextUIProvider } from "@nextui-org/react";
import { Session } from "next-auth";
import { SessionProvider } from "next-auth/react";

interface ProvidersProps {
  children: React.ReactNode;
  session?: Session | null;
}

export default function Providers({ children, session }: ProvidersProps) {
  return (
    <SessionProvider session={session}>
      <NextUIProvider>{children}</NextUIProvider>
    </SessionProvider>
  );
}

And in your layout.tsx:

export default async function RootLayout({
        children,
}: {
        children: React.ReactNode;
}) {
        const session = await getServerSession(
                authOptions
        );

return <Providers session={session}>{children}</Providers>

@getr62
Copy link
Author

getr62 commented Jan 20, 2024

Yea that's strange that the useSession hook is not getting the session after you sign in, but what you can do to avoid that custom loader is the following:

Since your Header component is a server component you can turn it into an async function and you can get the session directly in the component by awaiting the auth function from ./src/lib/auth.ts like this const session = await auth(); and passing the session as props to your <DynamicHeaderAuth session={session} /> component

Hi @bocarw121,

thank you for your in depth considerations.

The downside of this approach is that then the RootLayout which contains the header becomes a dynamic component, that means all pages become also dynamic pages. When running the build process of this solution we get the following output:
20-01-_2024_17-26-33

In the end that means we loose all the advantages of the more efficient static rendering of Next.js.

On my github project I pushed the new branch 'dynamic-header', which uses this approach. Actually it was a previous stage in the development of this project. When you look at the code of the mentioned branch there isn't yet the header-auth.tsx file but the whole code is just in the header.tsx. The code looks like this:

//  other imports . . .
import { auth } from '@/lib/auth';

export default async function Header() {
  const session = await auth();

  let authContent: React.ReactNode;
  if (session?.user) {
    authContent = (
      <Popover placement='left'>
        <PopoverTrigger>
          <Chip
            className='cursor-pointer'
            startContent={<Icon icon={pacman} />}
            variant='faded'
            color='default'
          >
            {session.user.name}
          </Chip>
        </PopoverTrigger>
        <PopoverContent>
          <div className='p-4'>
            <form action={actions.signOut}>
              <Button type='submit'>Sign Out</Button>
            </form>
          </div>
        </PopoverContent>
      </Popover>
    );
  } else {
    authContent = (
      <>
        <NavbarItem>
          <Link href={'/sign-in'}>Sign In</Link>
        </NavbarItem>

        <NavbarItem>
          <Link href={'/sign-up'}>Sign Up</Link>
        </NavbarItem>
      </>
    );
  }

  return (
    <Navbar className='shadow mb-6'>
      <NavbarBrand>
        <Link href='/' className='font-bold'>
          Discuss
        </Link>
      </NavbarBrand>
      <NavbarContent justify='center'>
        <NavbarItem>
          <Suspense>
            <SearchInput />
          </Suspense>
        </NavbarItem>
      </NavbarContent>

      <NavbarContent justify='end'>{authContent}</NavbarContent>
    </Navbar>
  );
}

In my initial post I forgot to mention, that the original project is taken from a course at Udemy.com which I took there. I only wrote this in the readme on github. I also forgot to mention that there only an OAuth-Provider was implemented and because of that the session strategy 'database' including the PrismaAdapter was used.

The interesting fact is that in the original project taken from Udemy everything is working fine with the useSession hook.

With regard to the authentication process I just took away the original auth provider as well as the in this case not applicable PrismaAdapter and put in the CredentialProvider.

@bocarw121
Copy link

@getr62

My pleasure, happy to see you found a solution!

@getr62
Copy link
Author

getr62 commented Jan 21, 2024

@bocarw121

No sorry, that is no solution for me at all. The 'solution' in my previous post was just a step while developing this demo project.

I would rather switch to another authentication package like https://lucia-auth.com or use a simple jsonwebtoken alone and and have to write the whole boilerplate code for this than loosing the advantages of static rendered pages in Next.js.

I know my English is bad because it is a foreign language for me. But what I wanted to say in my previous post was that when using

const session = await auth(); 

in the header.tsx file the build process leads to the following result:

dynamic

Whereas using

const session = useSession();

in a component marked with the 'use client' directive would lead to the folloing result:

static

So the logic to check the authentication state was cut out of the header.tsx file and put into the header-auth.tsx file.

The teacher in the Udemy course I mentioned used only the GithubProvider together with the PrismaAdapter and so the session strategy applied is 'database'. In such cases the call of the useSession hook just works fine and everything is ok.

As I mentioned in my initial post I will not be able to use Github, Google or all these other providers. The only option I am allowed to use in my app which I have to make is authentication with username and password because the app will not be reachable over a public website in the internet. It can only be accessed in a private network. Therefore I took away the GithubProvider and instead put in the CredentialProvider which switches the session strategy automatically to 'jwt'. But apparently auth.js has a bug (or feature?), that in this case the useSession hook can not retrieve the session.

@bocarw121
Copy link

Ahh okay gotcha, your english is good no worries.

I was thinking maybe you can try this and it will lead to the build results your expecting.

In your sign-in action instead of using the redirect() function, you can send a success result back to the client, which you can use in a useEffect to navigate to the home page causing a page refresh.

./actions/sign-in

"use server";

import type { Profile } from "@prisma/client";
import { z } from "zod";
import { db } from "@/db";
import { redirect } from "next/navigation";
import paths from "@/lib/paths";
import * as auth from "@/lib/auth";
import { compare } from "bcrypt";

const signInSchema = z.object({
  username: z.string().min(3),
  password: z.string().min(7),
});

interface SignInFormState {
  errors: {
    username?: string[];
    password?: string[];
    _form?: string[];
  };
  // Add this
  success?: boolean;
}

export async function signIn(
  formState: SignInFormState,
  formData: FormData
): Promise<SignInFormState> {
  const result = signInSchema.safeParse({
    username: formData.get("username"),
    password: formData.get("password"),
  });

  console.log("result signInSchema safeParse: ", result);

  if (!result.success) {
    return {
      errors: result.error.flatten().fieldErrors,
    };
  }

  const user = await db.user.findUnique({
    where: {
      name: result.data.username,
    },
  });

  if (!user) {
    return {
      errors: {
        _form: ["Wrong credentials"],
      },
    };
  }

  const passwordsMatch = await compare(result.data.password, user.password);

  if (!passwordsMatch) {
    return {
      errors: {
        _form: ["Wrong credentials"],
      },
    };
  }

  console.log("user found in sign-in action: ", user);

  let profile: Profile;
  try {
    await auth.signIn("credentials", {
      redirect: false,
      username: result.data.username,
      password: result.data.password,
    });

    profile = await db.profile.upsert({
      where: {
        userId: user.id,
      },
      update: {
        lastLogin: new Date().toLocaleString(),
      },
      create: {
        userId: user.id,
        lastLogin: new Date().toLocaleString(),
      },
    });
  } catch (err: unknown) {
    if (err instanceof Error) {
      console.log("error in auth.signIn action: ", err);
      return {
        errors: {
          _form: [err.message],
        },
      };
    } else {
      return {
        errors: {
          _form: ["Something went wrong"],
        },
      };
    }
  }

  return {
    errors: {},
    // add this
    success: true,
  };
}

and update the sign in form like this

./user/sign-in-form.tsx

import { useEffect } from "react";

  const [formState, action] = useFormState(actions.signIn, {
    errors: {},
    success: false,
  });

  useEffect(() => {
    if (formState.success) {
      // using replace so the user can't go back to the login page if they
      // press the back button
      window.location.replace("/");
    }
  }, [formState.success]);

As a side note the SessionProvider actually doesn't need the session as it retrieves it under the hood if the session prop is not passed, but if you did want to pass it and avoid all your pages being dynamic you can do this in your providers.tsx

"use client";

import { NextUIProvider } from "@nextui-org/react";
import { Session } from "next-auth";
import { SessionProvider, getSession } from "next-auth/react";
import { useCallback, useEffect, useState } from "react";

interface ProvidersProps {
  children: React.ReactNode;
}

export default function Providers({ children }: ProvidersProps) {
  const [session, setSession] = useState<Session | null>(null);

  const fetchSession = useCallback(async () => {
    const session = await getSession();
    setSession(session);
  }, []);

  useEffect(() => {
    fetchSession();
  }, [fetchSession]);

  return (
    <SessionProvider session={session}>
      <NextUIProvider>{children}</NextUIProvider>
    </SessionProvider>
  );
}

Hope this helps!

@getr62
Copy link
Author

getr62 commented Jan 21, 2024

Thank you so much @bocarw121 ,

you already helped me a lot!!! I took the useCurrentSession hook from your post a few days ago and put it without further changes into my project:

import { Session } from 'next-auth';
import { getSession } from 'next-auth/react';
import { usePathname } from 'next/navigation';
import { useState, useEffect, useCallback } from 'react';

// This hook doesn't rely on the session provider
export const useCurrentSession = () => {
  const [session, setSession] = useState<Session | null>(null);
  const [status, setStatus] = useState<string>('unauthenticated');
  const pathName = usePathname();

  const retrieveSession = useCallback(async () => {
    try {
      setStatus('loading');
      const sessionData = await getSession();

      if (sessionData) {
        setSession(sessionData);
        setStatus('authenticated');
        return;
      }

      setStatus('unauthenticated');
    } catch (error) {
      setStatus('unauthenticated');
      setSession(null);
    }
  }, []);

  useEffect(() => {
    retrieveSession();

    // use the pathname to force a re-render when the user navigates to a new page
  }, [retrieveSession, pathName]);

  return { session, status };
};

Then I changed the header-auth.tsx:

'use client';

import { useCurrentSession } from '@/hooks/useGetSession';
//other imports . . .

export default function HeaderAuth() {
  const { session, status } = useCurrentSession();

  let authContent: React.ReactNode;
  if (status === 'loading') {
    authContent = null;
  } else if (session && status === 'authenticated') {
    authContent = (
      <Popover placement='left'>
        <PopoverTrigger>
          <Chip
            className='cursor-pointer'
            startContent={<Icon icon={pacman} />}
            variant='faded'
            color='default'
          >
            {session!.user!.name}
          </Chip>
        </PopoverTrigger>
        <PopoverContent>
          <div className='p-4'>
            <form action={actions.signOut}>
              <Button type='submit'>Sign Out</Button>
            </form>
          </div>
        </PopoverContent>
      </Popover>
    );
  } else {
    authContent = (
      <>
        <NavbarItem>
          <Link href={'/sign-in'}>Sign In</Link>
        </NavbarItem>
        <NavbarItem>
          <Link href={'/sign-up'}>Sign Up</Link>
        </NavbarItem>
      </>
    );
  }

  return authContent;
}

Now the authentication works finally! I myself already experimented with the getSession function but I did not get it right with the combination of useState, useEffect and useCallback. I always run into an endless re-render loop.

If I were a girl I would say you are my hero ;) but so I will just say you are terrific awesome!

A small downer remains, because when navigating between pages there is a short flickering of the user button on the right side of the navigation bar:

flickering.navbar.when.switching.pages.mp4

Probably I need to write my own context provider for the session which the useCurrentSession returns to wrap the RootLayout within so all pages can immediately take the already available session?

But nevertheless I now have a working solution. Thank you so much again!

You should consider teaching your skills on sites like Udemy.com or do you have a Youtube channel?

I pushed the new branch "using-getSession" to my demo project on github where this working solution from you is implemented, so if anybody is interested can take a look there.

Now I will try to implement the suggestions of your last post :)

@bocarw121
Copy link

bocarw121 commented Jan 21, 2024

It's my pleasure! Happy to hear things are going the right away great job!

Thanks a lot I appreciate it 😄! I don't have any courses on Udemy or a YouTube but I will definitely consider doing that.

As far as the flash it's because the retrieveSession() function in the useCurrentSession() hook runs every time the path changes. I made a quick modification to the hook you can try it out.

./hooks/useCurrentSession

// This hook doesn't rely on the session provider
export const useCurrentSession = () => {
  const [session, setSession] = useState<Session | null>(null);
  // Changed the default status to loading
  const [status, setStatus] = useState<string>("loading");
  const pathName = usePathname();

  const retrieveSession = useCallback(async () => {
    try {
      const sessionData = await getSession();
      if (sessionData) {
        setSession(sessionData);
        setStatus("authenticated");
        return;
      }

      setStatus("unauthenticated");
    } catch (error) {
      setStatus("unauthenticated");
      setSession(null);
    }
  }, []);

  useEffect(() => {
    // We only want to retrieve the session when there is no session
    if (!session) {
      retrieveSession();
    }

    // use the pathname to force a re-render when the user navigates to a new page
  }, [retrieveSession, session, pathName]);

  return { session, status };
};

Also, if you implement the changes I gave you in the sign-in action and sign-in-form, you'll be able able to use the useSession hook directly.

Let me know if it helps.

@getr62
Copy link
Author

getr62 commented Jan 21, 2024

Now I say it, @bocarw121 , you are not only a hero ... you are a superhero!

The flashing issue is solved and everything runs smoothly!

Tomorrow I will implement the changes in the sign-in action and sign-in-form, because my cats demand my immediate attention. The whole day I neglected them besides feeding them (otherwise they would have eaten me) and me was just fixated on fixing my coding problems.

I can't express how happy and relieved I am feeling and how grateful to you I am.

As soon as I made these changes I will also let you know.

@bocarw121
Copy link

Sweet! That's awesome to hear!

Nice 😆 have a good time with your cats.

Yea, definitely keep me posted.

@DavidCodina
Copy link

DavidCodina commented Jan 27, 2024

Hello. The previous discussion was helpful. I'm using the v5 beta, and also running into issues with the server-side signIn(). It seems like it doesn't automatically update the SessionProvider, so I end up doing something like this in the LoginForm.

const handleCredentialsLogin = async (e: any) => {
  e.preventDefault()
  setLoading(true)

  try {
    // login() server action that calls server-side signIn() internally.
    const res = await login({ email, password })  

    if (res.success === false) {
      toast.error(res.message, { autoClose: false })
      return
    }

    await getSession()
    toast.success('Login success.')
    router.replace(DEFAULT_LOGIN_REDIRECT)
  } catch {
    toast.error('Unable to log in.', { autoClose: false })
  } finally {
    setLoading(false)
    setEmail('')
    setPassword('')
  }
}

I'm wondering if there's any way to sync the SessionProvider from the server (i.e., server action), rather than having to call await getSession() from the client on success. It would be really nice if the server-side signIn() just handled this automatically.

@enyelsequeira
Copy link

replace

Hello has there been any update on this, I am running through the same issue as you guys, my main problem is that supposedly next-auth should inject the session into the provider without you having to await getSession this aproach does work, just it shouldn't be like that perhaps and if it is docs would be highly appreciated? I also do not understand as to why this happens in some applications and not others, I have the same exact structure in 2 different code bases one works fine with the session being there and other one I had to this work out, any inputs would be great

Thanks!

@enyelsequeira
Copy link

for anyone that cares about this issue, the issue itself is the SessionProvider not updating correctly, I am assuming internally there are some issues with it. If you however create your ownSessionProvider this work, after working all day with it, It test it out like this
image
and just in case you want to see the App providers is using the default Sessionprovider given by next-auth that does not work
image

also you can see here how each components behave in the UI and code

image image

@JenniferMarston
Copy link

I had a related problem, where I was expecting useSession() to get the session from the server, but not the server cache. I think maybe the jwt callback documentation should mention that, useSession() does not guarantee the jwt callback will be invoked.
Currently it reads as if it will always be invoked, similar to getSession(), getServerSession()

Maybe add:

  • It's worth noting that the behavior of the useSession hook might not necessarily invoke the jwt callback on every call. The session data is cached, and subsequent calls to useSession within the same request or closely following requests might not trigger the callback if the session is still valid and available.

@davidnewcomb
Copy link

I hope I'm not hyjacking this thread but it seems there might be another (easier) way to demonstrate this problem.

I'm here from the email provider side. If I click the activation link in the email then it jumps from my email client onto a fresh page and I'm logged in with a valid session on the client and the server.

However, I'm looking at verification links being taken by link security checkers, see here. I encrypt the link and send the user to my custom verification page where the link is decoded and put under a Link. When the user clicks the link, it looks like Next does a fetch to get the anchor, rather than getting the page itself. The response is coming back with the location redirect but I don't think (or at least it dosen't look like) the cookie which is also coming back is being remembered as the client is moved onto the location page but the client session has not been updated with the new token.

This seems like the same problem. I was wondering if the signIn function could be used for the last step. Is this supported? as I could only find things saying signIn('email_provider_id', {email: '...'}) starts the flow but nothing mentioning ending the flow like you can with a credentials provider with username and password.

import { signIn } from 'next-auth/react'
signIn('email', {
      email: searchParams.email,
      token: searchParams.token
 })

@muadpn
Copy link

muadpn commented Mar 8, 2024

Problem: User SignIn Via login and password. -> return success to client. but doesn't update the session. Hard Refresh is required

Solution:
Try Changing server action signIn function to

"use server";
import { signIn } from "@/config/next-auth/auth";
export const loginAction =async (input: TLoginSchema) ={
  // validate user input
  try {
    await signIn("credentials", {
      email,
      password,
      redirectTo: DEFAULT_LOGIN_REDIRECT,
    });
  } catch (error) {
   if (error instanceof CredentialsSignin) {

      switch (error.code) {
        case "custom_error":
          return { error: "Please Verify your email" };
          // other custom cases you throw from SignIn including null value should be handled
        default:
          return { error: "Something went wrong!" };
      }
    }

    throw error;
  }
  }

The problem that client is not updating the session is because you need to throw a error in the end.
SignIn always throw a error, for redirection.

@AmandineTournay
Copy link

I noticed that problem recently too with a simple signIn inside the try/catch.

try {
	await signIn('credentials', {
		redirectTo: '/',
		email,
		password,
	})
} catch (error) {
	// ... Handle authentication errors from the provider

	console.log(error)
	throw error
}

When I log the error, I get this:

Error: NEXT_REDIRECT
    at getRedirectError (webpack-internal:///(action-browser)/./node_modules/next/dist/client/components/redirect.js:49:19)
    at redirect (webpack-internal:///(action-browser)/./node_modules/next/dist/client/components/redirect.js:60:11)
    at signIn (webpack-internal:///(action-browser)/./node_modules/next-auth/lib/actions.js:55:73)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async authenticate (webpack-internal:///(action-browser)/./src/actions/auth/sign-in/login.ts:47:9)
    at async /home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:39:1202
    at async rw (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:38:7927)
    at async r4 (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:41:1251)
    at async doRender (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/base-server.js:1429:30)
    at async cacheEntry.responseCache.get.routeKind (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/base-server.js:1578:40)
    at async DevServer.renderToResponseWithComponentsImpl (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/base-server.js:1498:28)
    at async DevServer.renderPageComponent (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/base-server.js:1915:24)
    at async DevServer.renderToResponseImpl (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/base-server.js:1953:32)
    at async DevServer.pipeImpl (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/base-server.js:900:25)
    at async NextNodeServer.handleCatchallRenderRequest (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/next-server.js:272:17)
    at async DevServer.handleRequestImpl (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/base-server.js:796:17)
    at async /home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/dev/next-dev-server.js:339:20
    at async Span.traceAsyncFn (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/trace/trace.js:154:20)
    at async DevServer.handleRequest (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/dev/next-dev-server.js:336:24)
    at async invokeRender (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/lib/router-server.js:174:21)
    at async handleRequest (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/lib/router-server.js:353:24)
    at async requestHandlerImpl (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/lib/router-server.js:377:13)
    at async Server.requestListener (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/lib/start-server.js:142:13) {
  digest: 'NEXT_REDIRECT;replace;http://localhost:3000/;303;',

The session on the back side with await auth() is working well, but useSession() can get only the data after a hard reload as the redirection is not running well I suppose ?

I do not know when happened that change from Next.js which prevent redirection from a try/catch block.

vercel/next.js#55586
https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#redirecting

@arashn0uri
Copy link

The session on the back side with await auth() is working well, but useSession() can get only the data after a hard reload as the redirection is not running well I suppose ?

I do not know when happened that change from Next.js which prevent redirection from a try/catch block.

same problem. any update?

@AmandineTournay
Copy link

I could fix that problem by replacing SessionProvider with a custom one instead (based from the original) for now.

// next-auth.d.ts

// Make the getCsrfToken accessible outside of next-auth package
declare module 'next-auth/react' {
	function getCsrfToken(): Promise<string>
}
// SessionDataWrapper.tsx (replace the current SessionProvider from next-auth/react)
'use client'

import React, { Context, createContext, type PropsWithChildren, useEffect, useMemo, useState } from 'react'
import { usePathname } from 'next/navigation'
import type { Session } from 'next-auth'
import { getCsrfToken } from 'next-auth/react'

/**
 * Provider props
 */
type TSessionProviderProps = PropsWithChildren<{
	session?: Session | null
}>

/**
 * Type of the returned Provider elements with data which contains session data, status that shows the state of the Provider, and update which is the function to update session data
 */
type TUpdateSession = (data?: any) => Promise<Session | null | undefined>
export type TSessionContextValue = { data: Session | null; status: string; update: TUpdateSession }

/**
 * React context to keep session through renders
 */
export const SessionContext: Context<TSessionContextValue | undefined> = createContext?.<TSessionContextValue | undefined>(undefined)

export function SessionDataProvider({ session: initialSession = null, children }: TSessionProviderProps) {
	const [session, setSession] = useState<Session | null>(initialSession)
	const [loading, setLoading] = useState<boolean>(!initialSession)
	const pathname: string = usePathname()

	useEffect(() => {
		const fetchSession = async () => {
			if (!initialSession) {
                                // Retrive data from session callback
				const fetchedSessionResponse: Response = await fetch('/api/auth/session')
				const fetchedSession: Session | null = await fetchedSessionResponse.json()

				setSession(fetchedSession)
				setLoading(false)
			}
		}

		fetchSession().finally()
	}, [initialSession, pathname])

	const sessionData = useMemo(
		() => ({
			data: session,
			status: loading ? 'loading' : session ? 'authenticated' : 'unauthenticated',
			async update(data?: any) {
				if (loading || !session) return

				setLoading(true)

				const fetchOptions: RequestInit = {
					headers: {
						'Content-Type': 'application/json',
					},
				}

				if (data) {
					fetchOptions.method = 'POST'
                                        // That is possible to replace getCsrfToken with a fetch to /api/auth/csrf
					fetchOptions.body = JSON.stringify({ csrfToken: await getCsrfToken(), data })
				}

				const fetchedSessionResponse: Response = await fetch('/api/auth/session', fetchOptions)
				let fetchedSession: Session | null = null

				if (fetchedSessionResponse.ok) {
					fetchedSession = await fetchedSessionResponse.json()

                                        setSession(fetchedSession)
					setLoading(false)
				}

				return fetchedSession
			},
		}),
		[loading, session],
	)

	return <SessionContext.Provider value={sessionData}>{children}</SessionContext.Provider>
}
// useSessionData.ts (replace useSession hook)
'use client'

import { useContext } from 'react'
import type { Session } from 'next-auth'
import { SessionContext, type TSessionContextValue } from '@/components/wrapper/SessionDataWrapper'

/**
 * Retrieve session data from the SessionContext for client side usage only.
 * Content:
 * ```
 *   {
 *     data: session [Session | null]
 *     status: 'authenticated' | 'unauthenticated' | 'loading'
 *     update: (data?: any) => Promise<Session | null | undefined>
 *   }
 * ```
 *
 * @throws {Error} - If React Context is unavailable in Server Components.
 * @throws {Error} - If `useSessionData` is not wrapped in a <SessionDataProvider /> where the error message is shown only in development mode.
 *
 * @returns {TSessionContextValue} - The session data obtained from the SessionContext.
 */
export function useSessionData(): TSessionContextValue {
	if (!SessionContext) {
		throw new Error('React Context is unavailable in Server Components')
	}

	const sessionContent: TSessionContextValue = useContext(SessionContext) || {
		data: null,
		status: 'unauthenticated',
		async update(): Promise<Session | null | undefined> {
			return undefined
		},
	}

	if (!sessionContent && process.env.NODE_ENV !== 'production') {
		throw new Error('[auth-wrapper-error]: `useSessionData` must be wrapped in a <SessionDataProvider />')
	}

	return sessionContent
}

And inside the root layout:

// app/layout.tsx
// In my case, as I use localized routes, everything is under app/[locale]
import React from 'react'
import type { ResolvingViewport } from 'next'
import { SessionDataProvider } from '@/components/wrapper/SessionDataWrapper'
import type { TLocalisation } from '@/schema/Localisation'
import { locales } from '@/schema/constants/Language'

import '@/styles/index.scss'

export async function generateViewport(): ResolvingViewport {
	return {
		width: 'device-width',
		height: 'device-height',
		initialScale: 1,
		maximumScale: 1,
		userScalable: false,
	}
}

export async function generateStaticParams() {
	return locales.map((language: TLocalisation) => ({ locale: language }))
}

export default async function RootLayout({ children }: React.PropsWithChildren) {
	return <SessionDataProvider>{children}</SessionDataProvider>
}

@ranlix
Copy link

ranlix commented May 11, 2024

I met the same issue.

Here is a line in source code:

if (loading || !session) return
image

that means if it is unauthenticated, you cannot call update function successfully.
Maybe there should be a forceUpdate function added in to useSession?

@ElsonNg-F
Copy link

I noticed that problem recently too with a simple signIn inside the try/catch.

try {
	await signIn('credentials', {
		redirectTo: '/',
		email,
		password,
	})
} catch (error) {
	// ... Handle authentication errors from the provider

	console.log(error)
	throw error
}

When I log the error, I get this:

Error: NEXT_REDIRECT
    at getRedirectError (webpack-internal:///(action-browser)/./node_modules/next/dist/client/components/redirect.js:49:19)
    at redirect (webpack-internal:///(action-browser)/./node_modules/next/dist/client/components/redirect.js:60:11)
    at signIn (webpack-internal:///(action-browser)/./node_modules/next-auth/lib/actions.js:55:73)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async authenticate (webpack-internal:///(action-browser)/./src/actions/auth/sign-in/login.ts:47:9)
    at async /home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:39:1202
    at async rw (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:38:7927)
    at async r4 (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:41:1251)
    at async doRender (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/base-server.js:1429:30)
    at async cacheEntry.responseCache.get.routeKind (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/base-server.js:1578:40)
    at async DevServer.renderToResponseWithComponentsImpl (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/base-server.js:1498:28)
    at async DevServer.renderPageComponent (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/base-server.js:1915:24)
    at async DevServer.renderToResponseImpl (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/base-server.js:1953:32)
    at async DevServer.pipeImpl (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/base-server.js:900:25)
    at async NextNodeServer.handleCatchallRenderRequest (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/next-server.js:272:17)
    at async DevServer.handleRequestImpl (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/base-server.js:796:17)
    at async /home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/dev/next-dev-server.js:339:20
    at async Span.traceAsyncFn (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/trace/trace.js:154:20)
    at async DevServer.handleRequest (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/dev/next-dev-server.js:336:24)
    at async invokeRender (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/lib/router-server.js:174:21)
    at async handleRequest (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/lib/router-server.js:353:24)
    at async requestHandlerImpl (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/lib/router-server.js:377:13)
    at async Server.requestListener (/home/atournay/Projects/Web/ste-home/home-front/node_modules/next/dist/server/lib/start-server.js:142:13) {
  digest: 'NEXT_REDIRECT;replace;http://localhost:3000/;303;',

The session on the back side with await auth() is working well, but useSession() can get only the data after a hard reload as the redirection is not running well I suppose ?

I do not know when happened that change from Next.js which prevent redirection from a try/catch block.

vercel/next.js#55586 https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#redirecting

Hi all, great discussion. I myself have encountered similar problems, namely:

  1. Error: NEXT_REDIRECT when using credentials signIn in a try/catch
  2. After login, useSession returns undefined until page is manually refreshed.
  3. The use of await auth() in root layout and SessionProvider causes the entire app to become dynamically rendered.

I am currently on NextJS v14.2.3 with next_auth/5.0.0-beta.18 and app routing.

The first and second issue seems to be linked to how redirects work in NextJS.
Here is an implementation that works for me:

  try {
      await signIn("credentials", { email, password, redirectTo: DEFAULT_LOGIN_REDIRECT});
  } catch (error) {

      if (isRedirectError(error)) {
          throw error;
      }

      if (error instanceof AuthError) {
          switch (error.type) {
              case "CredentialsSignin":
                  return { error: "Invalid email/password!" };
              default:
                  return { error: "Something went wrong while logging in!" };
          }
      }
      throw error;
  }

As for issue 3, the only workaround that gives the best of both worlds for me currently is to keep the use of SessionProvider as close to the components that require auth, instead of in the root layout. A similar issue was discussed here in the case of Clerk: https://github.com/orgs/clerk/discussions/1764.

Hope this helps and do let me know if there are better ways about this!

@ranlix
Copy link

ranlix commented May 22, 2024

Finally, I just added a custom-session-provider to work around

import { Session } from 'next-auth'
import { getSession as nextAuthGetSession, useSession } from 'next-auth/react'
import React, { ReactNode, createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'

type RefetchSession = () => Promise<void>

type CustomSessionContextValue =
  | { refetch: RefetchSession; data: Session; status: 'authenticated' }
  | {
      refetch: RefetchSession
      data: null
      status: 'unauthenticated' | 'loading'
    }

const CustomSessionContext = createContext<CustomSessionContextValue | undefined>(undefined)

interface CustomSessionProviderProps {
  children: ReactNode
}

/**
 * @description 由于 next-auth 的 useSession 获取是在 signIn 之前,所以 signIn 之后无法通过调用  update 重新获取
 * 所以需要自定义一个 useCustomSession 来对 session 进行 hack
 * @param param0
 * @returns
 */
export const CustomSessionProvider: React.FC<CustomSessionProviderProps> = ({ children }) => {
  const { data: _session, status: _status } = useSession()
  const [session, setSession] = useState<Session | null>(null)
  const [status, setStatus] = useState<'loading' | 'unauthenticated' | 'authenticated'>('loading')

  const fetchSession = useCallback(async () => {
    setStatus('loading')
    const session = await nextAuthGetSession()
    setSession(session)
    setStatus(session ? 'authenticated' : 'unauthenticated')
  }, [])

  useEffect(() => {
    setSession(_session)
    setStatus(_status)
  }, [_status, _session])

  const value: CustomSessionContextValue = useMemo(() => {
    if (session) {
      return {
        status: 'authenticated',
        data: session,
        refetch: fetchSession,
      }
    }
    if (status === 'loading') {
      return {
        status: 'loading',
        data: null,
        refetch: fetchSession,
      }
    }
    return {
      status: 'unauthenticated',
      data: null,
      refetch: fetchSession,
    }
  }, [fetchSession, session, status])

  return <CustomSessionContext.Provider value={value}>{children}</CustomSessionContext.Provider>
}

export const useCustomSession = (): CustomSessionContextValue => {
  const context = useContext(CustomSessionContext)
  if (!context) {
    throw new Error('useCustomSession must be used within a CustomSessionProvider')
  }
  return context
}
// _app.tsx

const MyApp: AppType<{ session: Session | null }> = ({ Component, pageProps: { session, ...pageProps } }) => {
  return (
    <SessionProvider session={session}>
      <CustomSessionProvider>
         <Component {...pageProps} />
      </CustomSessionProvider>
    </SessionProvider>
  )
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime.
Projects
None yet
Development

No branches or pull requests