[GH-ISSUE #2462] useSession runs on server & produces hydration errors #17833

Closed
opened 2026-04-15 16:10:35 -05:00 by GiteaMirror · 8 comments
Owner

Originally created by @papsavas on GitHub (Apr 27, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/2462

Is this suited for github?

  • Yes, this is suited for github

Additional context

To Reproduce

  1. setup better-auth on next.js
  2. create auth client
  3. destructure & export useSession (works correctly on authClient.useSession())
  4. use it in a client component
  5. console.log its content
  6. early return if isPending is true
  7. get a hydration mismatch between isPending early return (<div>) & following render (<SignInDiscord />)
//lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
  /** The base URL of the server (optional if you're using the same domain) */
  baseURL: "http://localhost:3000",
});

export const { signIn, signUp, signOut, useSession } = createAuthClient();
"use client";
import { signOut, useSession } from "@/lib/auth-client";
import { Button } from "../ui/button";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover";
import { useState } from "react";
import SignInDiscord from "../SignInDiscord";

const NavbarProfile = () => {
  const [open, setOpen] = useState(false);
  const { data: session, isPending, ...rest } = useSession();
  console.log({ isPending, session, ...rest }); //watch server logs
  if (isPending) return <div>Pending Auth...</div>;
  if (!session) return <SignInDiscord />;

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger className="cursor-pointer">
        <div className="flex items-center gap-2">{session.user.name}</div>
      </PopoverTrigger>
      <PopoverContent className="w-fit">
        <Button
          variant={"destructive"}
          onClick={async () => {
            signOut();
            setOpen(false);
          }}
        >
          SignOut
        </Button>
      </PopoverContent>
    </Popover>
  );
};

export default NavbarProfile;

Current vs. Expected behavior

Hooks on client components shouldn't run on the server. Instead, useSession runs resulting in:

{
  isPending: false,
  session: null,
  error: { status: 0, statusText: '' },
  isRefetching: false,
  refetch: [Function: refetch]
}

which messes up rendering and produces hydration errors.

What version of Better Auth are you using?

1.2.7

Provide environment information

- OS: Windows
- Browser: Chrome
- "next": "15.3.1",
- "react": "^19.1.0",

Which area(s) are affected? (Select all that apply)

Client

Auth config (if applicable)

export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: "mysql",
    schema,
  }),
  emailAndPassword: { enabled: true },
  socialProviders: {
    discord: {
      clientId: process.env.NEXT_PUBLIC_DISCORD_CLIENT_ID!,
      clientSecret: process.env.DISCORD_CLIENT_SECRET!,
    },
  },
  session: {
    expiresIn: 60 * 60 * 24 * 7,
    updateAge: 60 * 60 * 24,
  },
});
Originally created by @papsavas on GitHub (Apr 27, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/2462 ### Is this suited for github? - [x] Yes, this is suited for github ### Additional context - similar to #1713 - regression from #960 because of destructuring. Using `authClient.useSession` defaults `isPending` to `true` on server (correct behavior) - not a case of https://github.com/better-auth/better-auth/issues/1006#issuecomment-2742622552 interchange ### To Reproduce 1. [setup better-auth on next.js](https://www.better-auth.com/docs/integrations/next) 2. create auth client 3. **destructure & export** `useSession` (works correctly on `authClient.useSession()`) 4. use it in a client component 5. console.log its content 6. early return if `isPending` is true 7. get a hydration mismatch between isPending early return (`<div>`) & following render (`<SignInDiscord />`) ```tsx //lib/auth-client.ts import { createAuthClient } from "better-auth/react"; export const authClient = createAuthClient({ /** The base URL of the server (optional if you're using the same domain) */ baseURL: "http://localhost:3000", }); export const { signIn, signUp, signOut, useSession } = createAuthClient(); ``` ```tsx "use client"; import { signOut, useSession } from "@/lib/auth-client"; import { Button } from "../ui/button"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { useState } from "react"; import SignInDiscord from "../SignInDiscord"; const NavbarProfile = () => { const [open, setOpen] = useState(false); const { data: session, isPending, ...rest } = useSession(); console.log({ isPending, session, ...rest }); //watch server logs if (isPending) return <div>Pending Auth...</div>; if (!session) return <SignInDiscord />; return ( <Popover open={open} onOpenChange={setOpen}> <PopoverTrigger className="cursor-pointer"> <div className="flex items-center gap-2">{session.user.name}</div> </PopoverTrigger> <PopoverContent className="w-fit"> <Button variant={"destructive"} onClick={async () => { signOut(); setOpen(false); }} > SignOut </Button> </PopoverContent> </Popover> ); }; export default NavbarProfile; ``` ### Current vs. Expected behavior Hooks on client components shouldn't run on the server. Instead, `useSession` runs resulting in: ```js { isPending: false, session: null, error: { status: 0, statusText: '' }, isRefetching: false, refetch: [Function: refetch] } ``` which messes up rendering and produces hydration errors. ### What version of Better Auth are you using? 1.2.7 ### Provide environment information ```bash - OS: Windows - Browser: Chrome - "next": "15.3.1", - "react": "^19.1.0", ``` ### Which area(s) are affected? (Select all that apply) Client ### Auth config (if applicable) ```typescript export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "mysql", schema, }), emailAndPassword: { enabled: true }, socialProviders: { discord: { clientId: process.env.NEXT_PUBLIC_DISCORD_CLIENT_ID!, clientSecret: process.env.DISCORD_CLIENT_SECRET!, }, }, session: { expiresIn: 60 * 60 * 24 * 7, updateAge: 60 * 60 * 24, }, }); ```
GiteaMirror added the locked label 2026-04-15 16:10:35 -05:00
Author
Owner

@Kinfe123 commented on GitHub (Apr 27, 2025):

if you are next , it can be something related to nextjs runtime arch:

Server Components render exclusively on the server, generating static HTML for faster loads and reduced client-side JavaScript. They’re ideal for non-interactive, data-heavy parts of a page.
Client Components run on both the server (during SSR) and the client (for hydration). Their primary role is to add interactivity (state, effects, event handlers) while still benefiting from server-side rendering for SEO and initial load performance.

so client component can have a possiblitity of running on both environment with a undefinied preset values on server runtimes.
for more - https://github.com/vercel/next.js/discussions/54114

<!-- gh-comment-id:2833648744 --> @Kinfe123 commented on GitHub (Apr 27, 2025): if you are next , it can be something related to nextjs runtime arch: Server Components render exclusively on the server, generating static HTML for faster loads and reduced client-side JavaScript. They’re ideal for non-interactive, data-heavy parts of a page. Client Components run on both the server (during SSR) and the client (for hydration). Their primary role is to add interactivity (state, effects, event handlers) while still benefiting from server-side rendering for SEO and initial load performance. so client component can have a possiblitity of running on both environment with a undefinied preset values on server runtimes. for more - https://github.com/vercel/next.js/discussions/54114
Author
Owner

@papsavas commented on GitHub (Apr 28, 2025):

If useSession needs to run during SSR, the preset isPending value must be true (as resolved in #960).

isPending:false & session:null can only mean that there is no session and that this will persist during hydration too.

The nature of this issue might be tougher to deal with since it's a side effect of destructuring. Until then, authClient.useSession() seems to be the solution

<!-- gh-comment-id:2834000847 --> @papsavas commented on GitHub (Apr 28, 2025): If `useSession` needs to run during SSR, the preset `isPending` value must be `true` (as resolved in #960). `isPending:false` & `session:null` can only mean that there is no session and that this will persist during hydration too. The nature of this issue might be tougher to deal with since it's a side effect of destructuring. Until then, `authClient.useSession()` seems to be the solution
Author
Owner

@Kinfe123 commented on GitHub (Apr 28, 2025):

No. I'm explaining the reason why you are facing the server log during the render because of the next runtime alowing such convenience on such case as explained on issue

<!-- gh-comment-id:2834013866 --> @Kinfe123 commented on GitHub (Apr 28, 2025): No. I'm explaining the reason why you are facing the server log during the render because of the next runtime alowing such convenience on such case as explained on issue
Author
Owner

@Kinfe123 commented on GitHub (Apr 28, 2025):

Other than that . It is already explained the issue you mentioned about the mix of destructuring.

<!-- gh-comment-id:2834015293 --> @Kinfe123 commented on GitHub (Apr 28, 2025): Other than that . It is already explained the issue you mentioned about the mix of destructuring.
Author
Owner

@kstratis commented on GitHub (May 4, 2025):

Using the latest next js canary (15.4.0-canary.20 & better-auth v1.2.7), I'm also getting the hydration error even if I do authClient.useSession();.
This must definitely be a regression.

<!-- gh-comment-id:2849282379 --> @kstratis commented on GitHub (May 4, 2025): Using the latest next js canary (`15.4.0-canary.20` & `better-auth v1.2.7`), I'm also getting the hydration error even if I do `authClient.useSession();`. This must definitely be a regression.
Author
Owner

@samcxps commented on GitHub (May 9, 2025):

next@15.3.2 and better-auth@1.2.7 and also experiencing hydration issues when using hooks from the auth client. Noticed this only happens when trying to conditionally render different content based on the isPending property

i.e. This causes a hydration error.

  const router = useRouter();
  const session = authClient.useSession();

  if (session.isPending) {
    return (
      <div className="flex h-screen w-screen items-center justify-center">
        <Loader />
      </div>
    );
  }

return (
    <>
      <RedirectToSignIn />
      <SignedIn>
        <AuthenticatedProviders>
          <ErrorBoundary>{children}</ErrorBoundary>
          <PostHogIdentifier />
        </AuthenticatedProviders>
      </SignedIn>
    </>
  );
}

This does not cause a hydration error

  const router = useRouter();
  const session = authClient.useSession();

return (
    <>
      <RedirectToSignIn />
      <SignedIn>
        <AuthenticatedProviders>
          <ErrorBoundary>{children}</ErrorBoundary>
          <PostHogIdentifier />
        </AuthenticatedProviders>
      </SignedIn>
    </>
  );
}
<!-- gh-comment-id:2867781476 --> @samcxps commented on GitHub (May 9, 2025): `next@15.3.2` and `better-auth@1.2.7` and also experiencing hydration issues when using hooks from the auth client. Noticed this only happens when trying to conditionally render different content based on the `isPending` property i.e. This causes a hydration error. ```export default function Layout({ children }: PropsWithChildren) { const router = useRouter(); const session = authClient.useSession(); if (session.isPending) { return ( <div className="flex h-screen w-screen items-center justify-center"> <Loader /> </div> ); } return ( <> <RedirectToSignIn /> <SignedIn> <AuthenticatedProviders> <ErrorBoundary>{children}</ErrorBoundary> <PostHogIdentifier /> </AuthenticatedProviders> </SignedIn> </> ); } ``` This does not cause a hydration error ```export default function Layout({ children }: PropsWithChildren) { const router = useRouter(); const session = authClient.useSession(); return ( <> <RedirectToSignIn /> <SignedIn> <AuthenticatedProviders> <ErrorBoundary>{children}</ErrorBoundary> <PostHogIdentifier /> </AuthenticatedProviders> </SignedIn> </> ); } ```
Author
Owner

@asimaranov commented on GitHub (May 14, 2025):

Same one.

  const session = authClient.useSession();

<div
          className={cn(
            'group/rating z-1000 mx-2 flex h-8 items-center rounded-[0.3472vw] px-5',
            !!rating && 'hover:bg-secondary/10 relative',
            !!session.data?.user && 'cursor-pointer'
          )}
          onClick={() => {
            if (!session.data?.user) {
              return;
            }
            rating && setRating(undefined);
          }}
        >

Produces hydration error:
Image

<!-- gh-comment-id:2880853324 --> @asimaranov commented on GitHub (May 14, 2025): Same one. ```ts const session = authClient.useSession(); <div className={cn( 'group/rating z-1000 mx-2 flex h-8 items-center rounded-[0.3472vw] px-5', !!rating && 'hover:bg-secondary/10 relative', !!session.data?.user && 'cursor-pointer' )} onClick={() => { if (!session.data?.user) { return; } rating && setRating(undefined); }} > ``` Produces hydration error: <img width="1075" alt="Image" src="https://github.com/user-attachments/assets/c0633981-9dfa-4cf3-9270-c02fa97317d9" />
Author
Owner

@yerzham commented on GitHub (May 23, 2025):

I think https://github.com/better-auth/better-auth/pull/976 did not solve the hydration problem in the first place.

Could it be that the useSession state (which is using nanostore atom under the hood) is initialized in the server with default isPending: true, then it gets the session on the server leading to isPending: false, but the client still continues to initialize on the browser with isPending:true, even though server sent a rendered response with isPending: false leading to a hydration error.

Then there are two ways to fix the issue:

  • Server must always render with isPending: true no matter if session is obtained via nanostore atom and keep the client initialize with isPending: true.
  • Server must hydrate the isPending:true in server nanostore atom to the client's broswer nanostore atom

First one is probably easiest way to fix this. I am not fluent with nanostore atom, so I hope someone can check this.

Edit:
isPending: true is not the only thing to sync between server and client on hydration, session.data, session.error, session.refetch and others should all be checked/tested too.

<!-- gh-comment-id:2904620251 --> @yerzham commented on GitHub (May 23, 2025): I think https://github.com/better-auth/better-auth/pull/976 did not solve the hydration problem in the first place. Could it be that the useSession state (which is using nanostore atom under the hood) is initialized in the server with default `isPending: true`, then it gets the session on the server leading to `isPending: false`, but the client still continues to initialize on the browser with `isPending:true`, even though server sent a rendered response with `isPending: false` leading to a hydration error. Then there are two ways to fix the issue: - Server must always render with `isPending: true` no matter if session is obtained via nanostore atom and keep the client initialize with `isPending: true`. - Server must hydrate the `isPending:true` in server nanostore atom to the client's broswer nanostore atom First one is probably easiest way to fix this. I am not fluent with nanostore atom, so I hope someone can check this. Edit: `isPending: true` is not the only thing to sync between server and client on hydration, `session.data`, `session.error`, `session.refetch` and others should all be checked/tested too.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#17833