Hydration error in useListOrganizations hook #799

Closed
opened 2026-03-13 08:04:56 -05:00 by GiteaMirror · 10 comments
Owner

Originally created by @RicSala on GitHub (Mar 6, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Create a component that uses authClient.useListOrganizations()
  2. Implement conditional rendering based on loading states (isPending, isRefetching)
  3. Use this component in a server-rendered page
  4. Load the page and check browser console (do it multiple times, it does not happen always)
  5. See hydration error: "Hydration failed because the server rendered HTML didn't match the client"

Current vs. Expected behavior

Expected behavior
Either provide the same initial data on both server and client
Or defer showing loading states until after hydration is complete
Handle the synchronization internally in the hook implementation
Not require per-component workarounds for hydration issues

What version of Better Auth are you using?

1.2.3

Provide environment information

- OS: MacOs
 - Browser: Happening in both, Chrome and Safari (in incognito to prevent extensions messing around)

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

Client

Auth config (if applicable)

import { ContactTemplate } from '@repo/email/templates/contact';
import { betterAuth } from 'better-auth';
import { nextCookies } from 'better-auth/next-js';
import { organization } from 'better-auth/plugins';

import { database } from '@repo/database';
import { resend } from '@repo/email';
import { prismaAdapter } from 'better-auth/adapters/prisma';

// First define our auth instance
export const auth = betterAuth({
  basePath: '/api/auth',
  emailAndPassword: {
    enabled: true,
  },
  hooks: {},

  emailVerification: {
    sendVerificationEmail: async (data, _request) => {
      await resend.emails.send({
        from: 'ricardo@tet.com',
        to: data.user.email,
        subject: 'Verify your email',
        react: ContactTemplate({
          name: data.user.name,
          email: data.user.email,
          message: 'Please verify your email by clicking the link below:',
        }),
      });
    },
  },
  database: prismaAdapter(database, {
    provider: 'postgresql',
  }),
  plugins: [nextCookies(), organization()],
  //...add more options here
});


export { toNextJsHandler } from 'better-auth/next-js';


// client
export const authClient = createAuthClient({
  plugins: [organizationClient()],
});

Additional context

Screenshots

Error:

Image

Component:

Image

Workaround:

Image

Originally created by @RicSala on GitHub (Mar 6, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Create a component that uses authClient.useListOrganizations() 2. Implement conditional rendering based on loading states (isPending, isRefetching) 3. Use this component in a server-rendered page 4. Load the page and check browser console (do it multiple times, it does not happen always) 5. See hydration error: "Hydration failed because the server rendered HTML didn't match the client" ### Current vs. Expected behavior **Expected behavior** Either provide the same initial data on both server and client Or defer showing loading states until after hydration is complete Handle the synchronization internally in the hook implementation Not require per-component workarounds for hydration issues ### What version of Better Auth are you using? 1.2.3 ### Provide environment information ```bash - OS: MacOs - Browser: Happening in both, Chrome and Safari (in incognito to prevent extensions messing around) ``` ### Which area(s) are affected? (Select all that apply) Client ### Auth config (if applicable) ```typescript import { ContactTemplate } from '@repo/email/templates/contact'; import { betterAuth } from 'better-auth'; import { nextCookies } from 'better-auth/next-js'; import { organization } from 'better-auth/plugins'; import { database } from '@repo/database'; import { resend } from '@repo/email'; import { prismaAdapter } from 'better-auth/adapters/prisma'; // First define our auth instance export const auth = betterAuth({ basePath: '/api/auth', emailAndPassword: { enabled: true, }, hooks: {}, emailVerification: { sendVerificationEmail: async (data, _request) => { await resend.emails.send({ from: 'ricardo@tet.com', to: data.user.email, subject: 'Verify your email', react: ContactTemplate({ name: data.user.name, email: data.user.email, message: 'Please verify your email by clicking the link below:', }), }); }, }, database: prismaAdapter(database, { provider: 'postgresql', }), plugins: [nextCookies(), organization()], //...add more options here }); export { toNextJsHandler } from 'better-auth/next-js'; // client export const authClient = createAuthClient({ plugins: [organizationClient()], }); ``` ### Additional context **Screenshots** Error: <img width="1374" alt="Image" src="https://github.com/user-attachments/assets/c23e6b91-fecb-49e6-b7c4-3d3ba560f3d1" /> Component: ![Image](https://github.com/user-attachments/assets/5694320c-b519-45ab-a24e-6c2b99330813) Workaround: ![Image](https://github.com/user-attachments/assets/cbe31f6e-304c-49c3-b5f2-384e42e65e4c)
GiteaMirror added the bug label 2026-03-13 08:04:56 -05:00
Author
Owner

@mamlzy commented on GitHub (Mar 7, 2025):

Facing the same issue, when using isPending from useListOrganizations()

Image

The code:

'use client';

import React, { useState } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { authClient } from '@repo/auth/client';
import { PlusIcon } from 'lucide-react';
import { toast } from 'sonner';

import { CreateOrganizationDialog } from '@/components/create-organization-dialog';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';

export default function Page() {
  const router = useRouter();

  const { data: organizations, isPending } = authClient.useListOrganizations();

  const [showCreateOrganizationDialog, setShowCreateOrganizationDialog] =
    useState(false);

  const handleSelectOrg = async (
    e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>,
    organizationId: string
  ) => {
    await authClient.organization.setActive(
      {
        organizationId,
      },
      {
        onSuccess: () => {
          router.push(`${organizationId}`);
        },
        onError: (err) => {
          console.log('err =>', err);
          toast.error(err.error.message);
        },
      }
    );
  };

  return (
    <>
      <div className='flex min-h-svh justify-center'>
        <div className='w-full max-w-[1100px] py-8'>
          <div className='mb-8 flex items-center justify-between'>
            <p className='text-2xl font-bold'>Organization</p>
            <Button onClick={() => setShowCreateOrganizationDialog(true)}>
              <PlusIcon /> Create Organization
            </Button>
          </div>
          <div className='grid grid-cols-3 gap-4'>
            {isPending ? (
              <CardSkeleton />
            ) : (
              organizations?.map((organization, idx) => {
                const metadata: { primaryHexColor: string | null } =
                  organization.metadata
                    ? JSON.parse(organization.metadata)
                    : null;

                return (
                  <div
                    key={idx}
                    role='button'
                    tabIndex={0}
                    onKeyDown={(e) => {
                      if (e.key === 'Enter' || e.key === ' ') {
                        handleSelectOrg(e, organization.id);
                      }
                    }}
                    onClick={(e) => handleSelectOrg(e, organization.id)}
                    className='relative z-0 overflow-hidden rounded-xl border bg-background p-6 dark:bg-muted'
                  >
                    <div
                      className='absolute -right-32 -top-32 z-[-1] size-64 rounded-full bg-muted dark:bg-background'
                      style={{
                        backgroundColor: metadata.primaryHexColor
                          ? `#${metadata.primaryHexColor}`
                          : undefined,
                      }}
                    />
                    <Image
                      src={organization?.logo!}
                      alt='Logo Inspiry'
                      sizes='100vw'
                      className='mx-auto mb-6 size-40 rounded-full object-cover'
                      width={0}
                      height={0}
                      quality={100}
                      unoptimized
                    />
                    <div className='text-center'>
                      <p className='mb-6 text-xl font-semibold'>
                        {organization.name}
                      </p>
                      <p className='text-lg text-muted-foreground'>
                        No Description
                      </p>
                    </div>
                  </div>
                );
              })
            )}
          </div>
        </div>
      </div>

      <CreateOrganizationDialog
        show={showCreateOrganizationDialog}
        setShow={setShowCreateOrganizationDialog}
      />
    </>
  );
}

function CardSkeleton() {
  return Array.from({ length: 3 }).map((_, idx) => (
    <Skeleton key={idx} className='h-[350px] w-full' />
  ));
}
@mamlzy commented on GitHub (Mar 7, 2025): Facing the same issue, when using `isPending` from `useListOrganizations()` ![Image](https://github.com/user-attachments/assets/ca662532-51ea-4ddc-8445-5a42c6e45224) The code: ```ts 'use client'; import React, { useState } from 'react'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; import { authClient } from '@repo/auth/client'; import { PlusIcon } from 'lucide-react'; import { toast } from 'sonner'; import { CreateOrganizationDialog } from '@/components/create-organization-dialog'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; export default function Page() { const router = useRouter(); const { data: organizations, isPending } = authClient.useListOrganizations(); const [showCreateOrganizationDialog, setShowCreateOrganizationDialog] = useState(false); const handleSelectOrg = async ( e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>, organizationId: string ) => { await authClient.organization.setActive( { organizationId, }, { onSuccess: () => { router.push(`${organizationId}`); }, onError: (err) => { console.log('err =>', err); toast.error(err.error.message); }, } ); }; return ( <> <div className='flex min-h-svh justify-center'> <div className='w-full max-w-[1100px] py-8'> <div className='mb-8 flex items-center justify-between'> <p className='text-2xl font-bold'>Organization</p> <Button onClick={() => setShowCreateOrganizationDialog(true)}> <PlusIcon /> Create Organization </Button> </div> <div className='grid grid-cols-3 gap-4'> {isPending ? ( <CardSkeleton /> ) : ( organizations?.map((organization, idx) => { const metadata: { primaryHexColor: string | null } = organization.metadata ? JSON.parse(organization.metadata) : null; return ( <div key={idx} role='button' tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { handleSelectOrg(e, organization.id); } }} onClick={(e) => handleSelectOrg(e, organization.id)} className='relative z-0 overflow-hidden rounded-xl border bg-background p-6 dark:bg-muted' > <div className='absolute -right-32 -top-32 z-[-1] size-64 rounded-full bg-muted dark:bg-background' style={{ backgroundColor: metadata.primaryHexColor ? `#${metadata.primaryHexColor}` : undefined, }} /> <Image src={organization?.logo!} alt='Logo Inspiry' sizes='100vw' className='mx-auto mb-6 size-40 rounded-full object-cover' width={0} height={0} quality={100} unoptimized /> <div className='text-center'> <p className='mb-6 text-xl font-semibold'> {organization.name} </p> <p className='text-lg text-muted-foreground'> No Description </p> </div> </div> ); }) )} </div> </div> </div> <CreateOrganizationDialog show={showCreateOrganizationDialog} setShow={setShowCreateOrganizationDialog} /> </> ); } function CardSkeleton() { return Array.from({ length: 3 }).map((_, idx) => ( <Skeleton key={idx} className='h-[350px] w-full' /> )); } ```
Author
Owner

@sanjaydotpro commented on GitHub (Mar 10, 2025):

facing the same issue

@sanjaydotpro commented on GitHub (Mar 10, 2025): facing the same issue
Author
Owner

@JesperSvensson00 commented on GitHub (Mar 23, 2025):

Having same issues with useSession(). I think the problem is that isPending sometimes defaults to false on the server but default to true on the client. This does not happen every time for me though.

@JesperSvensson00 commented on GitHub (Mar 23, 2025): Having same issues with `useSession()`. I think the problem is that `isPending` sometimes defaults to false on the server but default to true on the client. This does not happen every time for me though.
Author
Owner

@mamlzy commented on GitHub (Mar 23, 2025):

Having same issues with useSession(). I think the problem is that isPending sometimes defaults to false on the server but default to true on the client. This does not happen every time for me though.

You right, facing the same issue too even on useSession hook

@mamlzy commented on GitHub (Mar 23, 2025): > Having same issues with `useSession()`. I think the problem is that `isPending` sometimes defaults to false on the server but default to true on the client. This does not happen every time for me though. You right, facing the same issue too even on `useSession` hook
Author
Owner

@dninomiya commented on GitHub (Mar 27, 2025):

facing the same issue too even on useListPasskeys hook

@dninomiya commented on GitHub (Mar 27, 2025): facing the same issue too even on useListPasskeys hook
Author
Owner

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

does this fixes your issue - https://github.com/better-auth/better-auth/pull/1714

@Kinfe123 commented on GitHub (Apr 11, 2025): does this fixes your issue - https://github.com/better-auth/better-auth/pull/1714
Author
Owner

@Bekacru commented on GitHub (Apr 12, 2025):

Hey all, make sure this is from better auth. I've seen some people reporting the same issue but the ones i've seen so far aren't related to these hooks but how they are handling the client/server renders

@Bekacru commented on GitHub (Apr 12, 2025): Hey all, make sure this is from better auth. I've seen some people reporting the same issue but the ones i've seen so far aren't related to these hooks but how they are handling the client/server renders
Author
Owner

@buiducnhat commented on GitHub (Apr 15, 2025):

Same issue, I think the issue comes from the isPending result when using the hook. So my temporary solution is remove them :)

@buiducnhat commented on GitHub (Apr 15, 2025): Same issue, I think the issue comes from the `isPending` result when using the hook. So my temporary solution is remove them :)
Author
Owner

@ping-maxwell commented on GitHub (Jul 8, 2025):

Hey all, can we confirm this is still an issue on latest?

@ping-maxwell commented on GitHub (Jul 8, 2025): Hey all, can we confirm this is still an issue on latest?
Author
Owner

@ping-maxwell commented on GitHub (Aug 11, 2025):

Closing as stale

@ping-maxwell commented on GitHub (Aug 11, 2025): Closing as stale
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#799