next-auth: SSR + useSession causes an extra network call and render cycle

Describe the bug

If you use getSession to return a session prop from getServerSideProps and then use useSession in the component, the useSession hook will cause the page to re-render twice (three times total) even though it returns the valid session object all three times.

In addition, the hook will still cause the client to make a network request to the server after its first render, even though it already has the session object.

Steps to reproduce

You can observe this behavior by adding a console log to the render method of /server in next-auth-example and watching network requests that it makes.

Expected behavior

As far as I can tell, the expected behavior would go like this for a server-side rendered page:

  1. the incoming request causes nextjs to to call getServerSideProps, which calls getSession and returns the session in pageProps.session which is passed into the context provider in _app.tsx
  2. nextjs renders the page on the server, where useSession returns [session, false] since it is able to retrieve the session object from the Provider context
  3. the client hydrates and renders for the first time, and useSession again returns [session, false] since it is able to retrieve the session object from the Provider context
  4. no additional renders or network requests are triggered

I could definitely be mistaken about what’s expected - but my understanding from the Provider documentation is that there should not be an extra network request.

Screenshots or error logs

What instead happens is that the client renders three times:

  1. on the first render, useSession returns [session, true] (where session is the full valid session object)
  2. the client dispatches a network request to /session
  3. the client re-renders, useSession returns [session, true] (the same as the first render)
  4. the network request resolves
  5. the client re-renders again, useSession returns [session, false]
E16F1738-F7C5-4250-8BEA-31C85B6EFB70-658-000107070BB4C866 AAD5D18A-7144-4A4F-B89C-DD71D87F0208-658-00010708B7239CDB

As another example, if you block network requests to /session, then the page first renders a logged-in view (once), and then a logged-out view (twice):

06364797-9E09-4CF4-955C-B195C73806CC-658-00010753FBBEAA16

These were tested on both development and production builds, without un-focusing or re-focusing the window. I can’t think of anything else that could be causing the re-renders.

Additional context

I think that #487 originally encountered the same root problem, although their complaint was about the session callback (on the sever) getting triggered several times and the discussion ended up addressing ways to avoid using the session callback altogether.

Adding a session callback with a console log to the example will show that it is invoked twice on every /server page request - once during SSR and then again when the client makes its network request after rendering once.

Feedback

  • Found the documentation helpful
  • Found documentation but was incomplete
  • Could not find relevant documentation
  • Found the example project helpful
  • Did not find the example project helpful

Thanks so much for all your work on this project! Overall it’s been a joy to use.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 7
  • Comments: 16 (4 by maintainers)

Commits related to this issue

Most upvoted comments

Hi there! I am writing this comment while I am using version 4, in fact, I am not entirely sure if my problem is related to what was presented in this problem or that the fixes that were introduced solve my problem, but despite all this, I am facing a similar problem, I will describe it. I use the useSession hook in order to get the session and store it in context, it works fine except it sends many extra requests to the /api/auth/session which are useless after actually getting the session for the first time, I mean the session is checked repeatedly whenever lose focus and refocus the window. I attached a screenshot of the successive requests.

Screenshot (113)

I am looking forward to getting rid of these requests, I hope this is clear.

I think doing it “right” does require some kind of fix or re-structuring on the next-auth side, but I can share a workaround I’ve been using in the meantime.

The basic goal is to write our own context that provides the session object.

The naive approach looks like this in _app.tsx:

import { getSession, Session } from "next-auth/client";

type ExpandedAppProps = AppProps & { session: Session };

const App = ({ Component, pageProps, session }: ExpandedAppProps) => {
	return (
		<MyCustomContext.Provider value={{ session }}>
			<Component {...pageProps} />
		</MyCustomContext.Provider>
	);
};

App.getInitialProps = async ({ ctx }: AppContext) => {
	const session = await getSession(ctx);
	return { session };
};

The problem with this is that there’s no way to access the session from getServerSideProps, since getServerSideProps is only passed a ctx: GetServerSidePropsContext object. I don’t think this ctx is always the same (===) one that is passed to App.getInitialProps, but the request object ctx.req definitely is the same for both.

What does this mean? It means this is a perfect place to use a WeakMap!

In some common file utils.ts:

import { Session, getSession } from "next-auth/client";

const sessionCache = new WeakMap<NextApiRequest, Session | null>()

export async function getCachedSession({ req }: { req: NextApiRequest }): Session | null {
  const session = sessionCache.get(req)
  if (sessionCache.has(req)) {
    return sessionCache.get(req)!
  } else {
    const session = await getSession({ req })
    sessionCache.set(req, session)
    return session
  }
}

then you can call await getCachedSession(ctx) from both App.getInitialProps and from getServerSideProps and it’ll only end up calling getSession once. Then on the client, we can get the session with useContext(MyCustomContext) whenever we need it.

Hope this helps! It’s pretty hacky - it’d be really great if the next-auth provider could encapsulate this.

(you might have to mess around with the types a little bit since I think ctx.req is a NextApiRequest when it’s in App.getInitialProps but just a plain IncomingMessage when it’s in getServerSideProps or something…)

@thilllon

Thanks, @ivan-kleshnin However, for what reasons should we Disable strict mode of React. ? https://reactjs.org/docs/strict-mode.html

The reason is React 18 with strict mode enabled will render components twice in dev environment. https://beta.reactjs.org/learn/keeping-components-pure#detecting-impure-calculations-with-strict-mode

We are working on making the next-auth core fully framework agnostic. 👍

you’ll be able to implement your own client however you want, or just use the REST api directly (that you can already do, see https://next-auth.js.org/getting-started/rest-api)

@3li7u it’s a classic case of “The road to hell is paved with good intentions”. React and Next-Auth try to help us a bit too much…

  1. Disable strict mode of React.
  2. useSession does not deduplicate queries. Don’t use SessionProvider and useSession and write your own useSession hook like https://github.com/nextauthjs/react-query/blob/main/index.js It can be based on React-Query or Apollo-Client or URQL or whatever you prefer.

👉 You really want to fetch & cache all data with a single “state-management-and-fetcher” tool (of your choice):

  • to rely on a single devtools and see all data there!
  • to rely on a single cross-tab sync tool!
  • to not rehydrate in 2 different ways after SSR!

To me, using an out-of-the-box useSession feels like using an out-of-the-box signin form. It’s approapriate as a one-off, dev-mode solution but it’s not intended to be a final code.


I especially question the design decision to have a SessionProvider that triggers the first fetch. In my mind no component named like *Provider* should ever fetch or do other effects.

Hi there! A reproduction repo with a link would be extremely helpful to debug your issue. 🙂

In your getServerSideProps, you should return the session in props. Please see the Next.js docs.

Then in your _app, you should pass pageProps.session to the Provider ({ Provider } from 'next-auth/client')

If you could verify the behavior you are describing with a minimal reproduction, then maybe it is something in next-auth.

UPDATE: sorry, I have to read through the OP’s comment a bit more thoroughly, you say it happens with our official example as well. I’ll have a look.