[GH-ISSUE #4625] Bug: useSession hook returns null on initial page load #9999

Closed
opened 2026-04-13 05:51:49 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @Nikola-Milovic on GitHub (Sep 13, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/4625

🐛 Bug: useSession hook returns null on initial page load, causing auth flicker

I'm experiencing an issue where the useSession hook returns a null session on the first render after a page navigation. This causes my application to briefly think the user is unauthenticated, leading to a flicker of unauthenticated content or incorrect redirects before the session data eventually loads.

The Problem

On every navigation to a protected route, the session is initially null. My components react to this by treating the user as logged out. Shortly after, the session loads correctly, and the state updates.

Here's the console log from a page that a user is redirected to, showing the state progression from null to the actual user data:

user: null
user undefined
userId null
user {name: …}
userId acd7adb1-88d2-4adb-96a3-a354e07017e9

This sequence demonstrates the problem: the initial state is unauthenticated.


Relevant Code

Here is the setup I'm using.

Page Component (useEffect log)

This is the code inside a component on a protected route that logs the user state.

import { useAppStore } from "@/stores/app";
import { useAuth } from "@/hooks/useAuth"; // Assuming useAuth is in hooks
import { useEffect } from "react";

// ... inside component
const { userId } = useAppStore();
const { user } = useAuth();

useEffect(() => {
  console.log("userId", userId);
  console.log("user", user);
}, [userId, user]);

AuthProvider

This is my main authentication context provider that wraps the application. It uses the useSession hook from better-auth.

import { createContext, useContext, type ReactNode, useEffect } from "react";
import { useSession, type Session } from "@/lib/auth";
import { useAppStore } from "@/stores/app";

interface AuthContextType {
  session: Session["session"] | undefined;
  user: Session["user"] | undefined;
  isLoading: boolean;
  isAuthenticated: boolean;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export function AuthProvider({ children }: { children: ReactNode }) {
  const { data: session, isPending } = useSession();
  const { setUserId } = useAppStore();

  const value = {
    session: session?.session,
    user: session?.user,
    isLoading: isPending,
    isAuthenticated: !!session,
  };

  useEffect(() => {
    if (session?.user) {
      setUserId(session.user.id);
    }
  }, [session, setUserId]);

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export function useAuth() {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error("useAuth must be used within an AuthProvider");
  }
  return context;
}

Auth Library Setup (lib/auth.ts)

This file configures and exports the better-auth client.

import { createAuthClient } from "better-auth/react";
import { getConfig } from "@/lib/config";
import type { Auth } from "@packz/api/auth";
import { customSessionClient } from "better-auth/client/plugins";

const config = getConfig();

export const authClient = createAuthClient({
  baseURL: config.authUrl,
  plugins: [customSessionClient<Auth>()],
});

export const { 
  useSession, 
  signIn, 
  signUp, 
  signOut, 
  forgetPassword, 
  resetPassword, 
  changeEmail, 
  changePassword 
} = authClient;

export const authFetch = authClient.$fetch;

export type Session = typeof authClient.$Infer.Session;
export type User = typeof authClient.$Infer.Session.user;

How can I correctly handle the initial loading state of the session? My goal is to prevent the "flicker" where my app briefly believes the user is logged out. Should my AuthProvider show a loader while isPending is true, or is there a different pattern I should be using with better-auth to wait for the session to be resolved before rendering the page?

Package Version: better-auth: ^1.3.9

Originally created by @Nikola-Milovic on GitHub (Sep 13, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/4625 ### 🐛 Bug: `useSession` hook returns `null` on initial page load, causing auth flicker I'm experiencing an issue where the `useSession` hook returns a `null` session on the first render after a page navigation. This causes my application to briefly think the user is unauthenticated, leading to a flicker of unauthenticated content or incorrect redirects before the session data eventually loads. ## The Problem On every navigation to a protected route, the session is initially `null`. My components react to this by treating the user as logged out. Shortly after, the session loads correctly, and the state updates. Here's the console log from a page that a user is redirected to, showing the state progression from `null` to the actual user data: ``` user: null user undefined userId null user {name: …} userId acd7adb1-88d2-4adb-96a3-a354e07017e9 ``` This sequence demonstrates the problem: the initial state is unauthenticated. ----- ## Relevant Code Here is the setup I'm using. #### Page Component (`useEffect` log) This is the code inside a component on a protected route that logs the user state. ```typescript import { useAppStore } from "@/stores/app"; import { useAuth } from "@/hooks/useAuth"; // Assuming useAuth is in hooks import { useEffect } from "react"; // ... inside component const { userId } = useAppStore(); const { user } = useAuth(); useEffect(() => { console.log("userId", userId); console.log("user", user); }, [userId, user]); ``` ----- #### AuthProvider This is my main authentication context provider that wraps the application. It uses the `useSession` hook from `better-auth`. ```typescript import { createContext, useContext, type ReactNode, useEffect } from "react"; import { useSession, type Session } from "@/lib/auth"; import { useAppStore } from "@/stores/app"; interface AuthContextType { session: Session["session"] | undefined; user: Session["user"] | undefined; isLoading: boolean; isAuthenticated: boolean; } const AuthContext = createContext<AuthContextType | undefined>(undefined); export function AuthProvider({ children }: { children: ReactNode }) { const { data: session, isPending } = useSession(); const { setUserId } = useAppStore(); const value = { session: session?.session, user: session?.user, isLoading: isPending, isAuthenticated: !!session, }; useEffect(() => { if (session?.user) { setUserId(session.user.id); } }, [session, setUserId]); return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; } export function useAuth() { const context = useContext(AuthContext); if (context === undefined) { throw new Error("useAuth must be used within an AuthProvider"); } return context; } ``` ----- #### Auth Library Setup (`lib/auth.ts`) This file configures and exports the `better-auth` client. ```typescript import { createAuthClient } from "better-auth/react"; import { getConfig } from "@/lib/config"; import type { Auth } from "@packz/api/auth"; import { customSessionClient } from "better-auth/client/plugins"; const config = getConfig(); export const authClient = createAuthClient({ baseURL: config.authUrl, plugins: [customSessionClient<Auth>()], }); export const { useSession, signIn, signUp, signOut, forgetPassword, resetPassword, changeEmail, changePassword } = authClient; export const authFetch = authClient.$fetch; export type Session = typeof authClient.$Infer.Session; export type User = typeof authClient.$Infer.Session.user; ``` ----- How can I correctly handle the initial **loading state** of the session? My goal is to prevent the "flicker" where my app briefly believes the user is logged out. Should my `AuthProvider` show a loader while `isPending` is true, or is there a different pattern I should be using with `better-auth` to wait for the session to be resolved before rendering the page? **Package Version:** `better-auth`: `^1.3.9`
GiteaMirror added the lockedbug labels 2026-04-13 05:51:50 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Sep 13, 2025):

Hi @Nikola-Milovic! I'm Dosu and I’m helping the better-auth team.

This flicker happens because the useSession hook always fetches session data on mount, so the session is null until the request completes—even if the user is already authenticated. This is a known behavior and can cause the UI to briefly show unauthenticated content or trigger redirects before the session loads [source].

The best practice is to wrap your app in a custom SessionProvider that manages session state and loading internally, then expose a custom useSession hook from that provider. This way, you can control when your app renders protected content and avoid flicker. The provider tracks isPending and session state, and you can show a loader or fallback UI while isPending is true. This pattern also prevents duplicate session fetches and hydration mismatches [source].

Here's a simplified example based on the recommended pattern:

import React, { createContext, useContext, useEffect, useState, ReactNode, useCallback } from "react";
import { useSession as useBetterAuthSession } from "@/lib/auth";

// Context and Provider
const SessionContext = createContext(null);

export function SessionProvider({ children }) {
  const [isPending, setIsPending] = useState(true);
  const [data, setData] = useState(null);

  const onUpdate = useCallback((pending, nextData) => {
    setIsPending(pending);
    setData(nextData);
  }, []);

  return (
    <SessionContext.Provider value={{ isPending, data }}>
      {children}
      <SessionBridge onUpdate={onUpdate} />
    </SessionContext.Provider>
  );
}

// Bridge to sync with better-auth's useSession
function SessionBridge({ onUpdate }) {
  const { isPending, data } = useBetterAuthSession();
  useEffect(() => {
    onUpdate(isPending, data ?? null);
  }, [isPending, data, onUpdate]);
  return null;
}

// Custom hook
export function useSession() {
  const context = useContext(SessionContext);
  if (!context) throw new Error("useSession must be used within a SessionProvider");
  return context;
}

Wrap your app with SessionProvider at the root, and use the custom useSession everywhere instead of the direct better-auth hook. Show a loader or fallback UI while isPending is true to prevent flicker. This approach is the recommended way to handle session loading state with better-auth [source].

If you need SSR compatibility or want to hydrate with initial session data, you can also provide the session from the server to the provider to avoid the initial fetch [source].

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

<!-- gh-comment-id:3287719749 --> @dosubot[bot] commented on GitHub (Sep 13, 2025): <!-- Greeting --> Hi @Nikola-Milovic! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This flicker happens because the `useSession` hook always fetches session data on mount, so the session is `null` until the request completes—even if the user is already authenticated. This is a known behavior and can cause the UI to briefly show unauthenticated content or trigger redirects before the session loads [[source](https://github.com/better-auth/better-auth/issues/2768)]. The best practice is to wrap your app in a custom `SessionProvider` that manages session state and loading internally, then expose a custom `useSession` hook from that provider. This way, you can control when your app renders protected content and avoid flicker. The provider tracks `isPending` and `session` state, and you can show a loader or fallback UI while `isPending` is true. This pattern also prevents duplicate session fetches and hydration mismatches [[source](https://github.com/better-auth/better-auth/issues/4609)]. Here's a simplified example based on the recommended pattern: ```tsx import React, { createContext, useContext, useEffect, useState, ReactNode, useCallback } from "react"; import { useSession as useBetterAuthSession } from "@/lib/auth"; // Context and Provider const SessionContext = createContext(null); export function SessionProvider({ children }) { const [isPending, setIsPending] = useState(true); const [data, setData] = useState(null); const onUpdate = useCallback((pending, nextData) => { setIsPending(pending); setData(nextData); }, []); return ( <SessionContext.Provider value={{ isPending, data }}> {children} <SessionBridge onUpdate={onUpdate} /> </SessionContext.Provider> ); } // Bridge to sync with better-auth's useSession function SessionBridge({ onUpdate }) { const { isPending, data } = useBetterAuthSession(); useEffect(() => { onUpdate(isPending, data ?? null); }, [isPending, data, onUpdate]); return null; } // Custom hook export function useSession() { const context = useContext(SessionContext); if (!context) throw new Error("useSession must be used within a SessionProvider"); return context; } ``` Wrap your app with `SessionProvider` at the root, and use the custom `useSession` everywhere instead of the direct `better-auth` hook. Show a loader or fallback UI while `isPending` is true to prevent flicker. This approach is the recommended way to handle session loading state with `better-auth` [[source](https://github.com/better-auth/better-auth/issues/4609)]. If you need SSR compatibility or want to hydrate with initial session data, you can also provide the session from the server to the provider to avoid the initial fetch [[source](https://github.com/better-auth/better-auth/issues/2768)]. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/c7c1eddb-b10b-4b28-acae-8ecccc80c9c2?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/c7c1eddb-b10b-4b28-acae-8ecccc80c9c2?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/c7c1eddb-b10b-4b28-acae-8ecccc80c9c2?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/c7c1eddb-b10b-4b28-acae-8ecccc80c9c2?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/c7c1eddb-b10b-4b28-acae-8ecccc80c9c2?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/c7c1eddb-b10b-4b28-acae-8ecccc80c9c2?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/c7c1eddb-b10b-4b28-acae-8ecccc80c9c2?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/4625)
Author
Owner

@himself65 commented on GitHub (Sep 13, 2025):

Right now, it's the expected behavior.

<!-- gh-comment-id:3287749142 --> @himself65 commented on GitHub (Sep 13, 2025): Right now, it's the expected behavior.
Author
Owner

@Nikola-Milovic commented on GitHub (Sep 13, 2025):

@himself65 Ah, that's a bit weird, is it at least documented anywhere with a solution like the bot gave me?

<!-- gh-comment-id:3287754355 --> @Nikola-Milovic commented on GitHub (Sep 13, 2025): @himself65 Ah, that's a bit weird, is it at least documented anywhere with a solution like the bot gave me?
Author
Owner

@himself65 commented on GitHub (Sep 13, 2025):

Yeah we gonna improve it soon

<!-- gh-comment-id:3287760699 --> @himself65 commented on GitHub (Sep 13, 2025): Yeah we gonna improve it soon
Author
Owner

@SorooshGb commented on GitHub (Sep 14, 2025):

useSession() sends a GET request to /api/auth/get-session, so the session will be null initially while the fetch is still in progress. That’s why it also returns isPending. You should always check that isPending is false before accessing session. That’s the current behavior we need to work with.

<!-- gh-comment-id:3289385819 --> @SorooshGb commented on GitHub (Sep 14, 2025): `useSession()` sends a GET request to `/api/auth/get-session`, so the session will be null initially while the fetch is still in progress. That’s why it also returns isPending. You should always check that isPending is false before accessing session. That’s the current behavior we need to work with.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#9999