[GH-ISSUE #986] useSession on client return cached session instead of fetching #25858

Closed
opened 2026-04-17 16:08:42 -05:00 by GiteaMirror · 20 comments
Owner

Originally created by @daveycodez on GitHub (Dec 21, 2024).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/986

Is this suited for github?

  • Yes, this is suited for github

No response

Describe the solution you'd like

If I'm using the useSession hook, whenever I refresh the page it sends an API request to get-session. We should already have the session locally, though. A comparison would be Supabase, their useSession hook does not send any requests unless the session is expired. Having to fetch the session when it already exists locally will make sites feel slower on initial load, and incur unnecessary bandwidth on the API. useSession needs to return the cached session on mount

Describe alternatives you've considered

useSession hook return cached session immediately on client, only fetch get-session if it is expired.

Additional context

No response

Originally created by @daveycodez on GitHub (Dec 21, 2024). Original GitHub issue: https://github.com/better-auth/better-auth/issues/986 ### Is this suited for github? - [x] Yes, this is suited for github ### Is your feature request related to a problem? Please describe. _No response_ ### Describe the solution you'd like If I'm using the useSession hook, whenever I refresh the page it sends an API request to get-session. We should already have the session locally, though. A comparison would be Supabase, their useSession hook does not send any requests unless the session is expired. Having to fetch the session when it already exists locally will make sites feel slower on initial load, and incur unnecessary bandwidth on the API. useSession needs to return the cached session on mount ### Describe alternatives you've considered useSession hook return cached session immediately on client, only fetch get-session if it is expired. ### Additional context _No response_
GiteaMirror added the locked label 2026-04-17 16:08:42 -05:00
Author
Owner

@Bekacru commented on GitHub (Dec 22, 2024):

The session cache is stored as an HTTP-only cookie, and the server needs to validate that the token is properly signed before sending back the session data. If there is no API request, it would mean storing the session in a way that js can read it, which isn’t ideal and is not recommended. Supabase do this mostly cause they don't have any other option since 3rd party cookies nowdays get blocked by browsers.

<!-- gh-comment-id:2558355879 --> @Bekacru commented on GitHub (Dec 22, 2024): The session cache is stored as an HTTP-only cookie, and the server needs to validate that the token is properly signed before sending back the session data. If there is no API request, it would mean storing the session in a way that js can read it, which isn’t ideal and is not recommended. Supabase do this mostly cause they don't have any other option since 3rd party cookies nowdays get blocked by browsers.
Author
Owner

@daveycodez commented on GitHub (Dec 22, 2024):

Makes sense. I'm also seeing that it's unsafe to store JWT in localStorage so I need to update some things there. Is it possible to have get-session return the jwt as well? I also saw examples of storing Bearer in localStorage, isn't that vulnerable to XSS attacks?

<!-- gh-comment-id:2558365223 --> @daveycodez commented on GitHub (Dec 22, 2024): Makes sense. I'm also seeing that it's unsafe to store JWT in localStorage so I need to update some things there. Is it possible to have get-session return the jwt as well? I also saw examples of storing Bearer in localStorage, isn't that vulnerable to XSS attacks?
Author
Owner

@Bekacru commented on GitHub (Dec 22, 2024):

Yeah, it’s vulnerable to XSS, but it’s all about trade-offs. If an attacker manages to pull off an XSS attack, they can already send requests directly to your server, so stealing tokens might not even be even that important. They can do a lot of things. So, locking down your site against XSS should be the first priorit and it’s pretty straightforward these days. And, JWTs can easily get too large for cookies, so local storage makes more sense. It’s not perfect, but as long as you’re aware of the risks and have proper security measures in place, it’s a workable option.

<!-- gh-comment-id:2558399601 --> @Bekacru commented on GitHub (Dec 22, 2024): Yeah, it’s vulnerable to XSS, but it’s all about trade-offs. If an attacker manages to pull off an XSS attack, they can already send requests directly to your server, so stealing tokens might not even be even that important. They can do a lot of things. So, locking down your site against XSS should be the first priorit and it’s pretty straightforward these days. And, JWTs can easily get too large for cookies, so local storage makes more sense. It’s not perfect, but as long as you’re aware of the risks and have proper security measures in place, it’s a workable option.
Author
Owner

@daveycodez commented on GitHub (Dec 22, 2024):

In this case wouldn't it also be fine to store the sessionData cached on localStorage so it can be used immediately on page load, so other requests (e.g. getMyProfile /api/profile/me) aren't dependent on waiting for that first getSession API request to resolve. Something like useOptimisticSession that returns it from localStorage and gets revalidated instead of starting as null.

Right now it goes like ->

(Slow Network example on edit-profile page)

  1. Wait for Page Load (300ms)
  2. Wait for getSession API Request (network latency - 300ms)
  3. Get Current Profile from API (custom profiles sql table) (300ms)
    Total time to render - 900ms

Whereas if we had it cached it would be...

  1. Wait for Page Load (300ms)
  2. Load cached sessionData (disk read latency) (~0ms)
  3. Fire getSession (revalidate) and getCurrentProfile simultaneously (~300ms)
    Total time to render - ~600ms

Another example is headers with Avatars (image field).

  1. Use cached sessionData (~0ms)
  2. Render avatar (~0ms after page load) & revalidate getSession

vs currently

  1. Wait for getSession(300ms)
  2. Render Avatar (300ms time to render after page load)

I know I could make my own useOptimisticSession hook and cache it in localStorage but wouldn't it be better for all sites using better-auth to have faster perceived load times, or maybe even a config option for optimisticSessions (boolean)

<!-- gh-comment-id:2558576456 --> @daveycodez commented on GitHub (Dec 22, 2024): In this case wouldn't it also be fine to store the sessionData cached on localStorage so it can be used immediately on page load, so other requests (e.g. getMyProfile /api/profile/me) aren't dependent on waiting for that first getSession API request to resolve. Something like useOptimisticSession that returns it from localStorage and gets revalidated instead of starting as null. Right now it goes like -> **(Slow Network example on edit-profile page)** 1. Wait for Page Load (300ms) 2. Wait for getSession API Request (network latency - 300ms) 3. Get Current Profile from API (custom profiles sql table) (300ms) Total time to render - 900ms **Whereas if we had it cached it would be...** 1. Wait for Page Load (300ms) 2. Load cached sessionData (disk read latency) (~0ms) 3. Fire getSession (revalidate) and getCurrentProfile simultaneously (~300ms) Total time to render - ~600ms ### Another example is headers with Avatars (image field). 1. Use cached sessionData (~0ms) 2. Render avatar (~0ms after page load) & revalidate getSession **vs currently** 1. Wait for getSession(300ms) 2. Render Avatar (300ms time to render after page load) I know I could make my own useOptimisticSession hook and cache it in localStorage but wouldn't it be better for all sites using better-auth to have faster perceived load times, or maybe even a config option for optimisticSessions (boolean)
Author
Owner

@daveycodez commented on GitHub (Dec 22, 2024):

The only concern I think with using localStorage is storing PII like email? But storing the JWT would also do that in its current state?

use-optimistic-session.ts

import { useEffect, useState } from "react"
import { useSession } from "@/lib/auth-client"

export function useOptimisticSession() {
    const { data: sessionData, isPending: sessionPending } = useSession()
    const [session, setSession] = useState(sessionData)
    const [isPending, setIsPending] = useState(true)

    // Load session from localStorage
    useEffect(() => {
        if (session) return

        const cachedSession = localStorage.getItem("session")

        if (cachedSession) {
            const sessionData = JSON.parse(cachedSession)
            if (new Date(sessionData.session.expiresAt) < new Date()) {
                localStorage.removeItem("session")
                return
            }

            setSession(sessionData)
            setIsPending(false)
        }
    }, [session])

    // Update session on revalidation
    useEffect(() => {
        if (isPending) setIsPending(sessionPending)
        if (sessionPending) return

        setSession(sessionData)

        if (sessionData) {
            localStorage.setItem("session", JSON.stringify(sessionData))
        } else {
            localStorage.removeItem("session")
        }
    }, [isPending, sessionData, sessionPending])

    return { session, isPending }
}
<!-- gh-comment-id:2558582677 --> @daveycodez commented on GitHub (Dec 22, 2024): The only concern I think with using localStorage is storing PII like email? But storing the JWT would also do that in its current state? use-optimistic-session.ts ```ts import { useEffect, useState } from "react" import { useSession } from "@/lib/auth-client" export function useOptimisticSession() { const { data: sessionData, isPending: sessionPending } = useSession() const [session, setSession] = useState(sessionData) const [isPending, setIsPending] = useState(true) // Load session from localStorage useEffect(() => { if (session) return const cachedSession = localStorage.getItem("session") if (cachedSession) { const sessionData = JSON.parse(cachedSession) if (new Date(sessionData.session.expiresAt) < new Date()) { localStorage.removeItem("session") return } setSession(sessionData) setIsPending(false) } }, [session]) // Update session on revalidation useEffect(() => { if (isPending) setIsPending(sessionPending) if (sessionPending) return setSession(sessionData) if (sessionData) { localStorage.setItem("session", JSON.stringify(sessionData)) } else { localStorage.removeItem("session") } }, [isPending, sessionData, sessionPending]) return { session, isPending } } ```
Author
Owner

@Bekacru commented on GitHub (Dec 23, 2024):

I think this should currently be handled by users rather than from the lib. Specifically, revalidating the cache becomes a problem. for example, if the user updates their info, the cookie cache will get overridden by the server. To do the same on the client, we would then need to track which endpoint is being calle and invalidate it which introduces a lot of complexity.

<!-- gh-comment-id:2558949734 --> @Bekacru commented on GitHub (Dec 23, 2024): I think this should currently be handled by users rather than from the lib. Specifically, revalidating the cache becomes a problem. for example, if the user updates their info, the cookie cache will get overridden by the server. To do the same on the client, we would then need to track which endpoint is being calle and invalidate it which introduces a lot of complexity.
Author
Owner

@daveycodez commented on GitHub (Dec 23, 2024):

Since I'm just caching sessionData and refreshing it wouldn't it be the same outcome though? It still calls getSession on page load, which gets the latest session data and overrides the localStorage cache with the new data.

Another thing I think we need is a way to manually refresh the session coming from useSession hook. Like a refresh or reload function. In SWR they have revalidations trigger on page focus. Currently the useSession hook won't show you an updated email address or any other updated field to the user unless you do a hard refresh.

I definitely think default built in functionality for caching sessionData, revalidating on page focus, and proper retry on network failure is a necessity for any modern web application

<!-- gh-comment-id:2559258782 --> @daveycodez commented on GitHub (Dec 23, 2024): Since I'm just caching sessionData and refreshing it wouldn't it be the same outcome though? It still calls getSession on page load, which gets the latest session data and overrides the localStorage cache with the new data. Another thing I think we need is a way to manually refresh the session coming from useSession hook. Like a refresh or reload function. In SWR they have revalidations trigger on page focus. Currently the useSession hook won't show you an updated email address or any other updated field to the user unless you do a hard refresh. I definitely think default built in functionality for caching sessionData, revalidating on page focus, and proper retry on network failure is a necessity for any modern web application
Author
Owner

@Bekacru commented on GitHub (Dec 23, 2024):

If you need to control how session is cached and how and when refetch is triggered including onFocus and other advanced functionalities, you can use getSession with swr or react query instead. Said that, the session is cached in memory as it's with swr. Meaning calling useSession in two components doesn't trigger 2 different fetch requests. But local storage caching is a different story. We should probably mention using those libraries for advanced use cases for fetching session on the client.

<!-- gh-comment-id:2559287125 --> @Bekacru commented on GitHub (Dec 23, 2024): If you need to control how session is cached and how and when refetch is triggered including onFocus and other advanced functionalities, you can use `getSession` with swr or react query instead. Said that, the session is cached in memory as it's with swr. Meaning calling `useSession` in two components doesn't trigger 2 different fetch requests. But local storage caching is a different story. We should probably mention using those libraries for advanced use cases for fetching session on the client.
Author
Owner

@daveycodez commented on GitHub (Dec 23, 2024):

Yea I know I can write the hooks, but I was kind of hoping to see this built in since everyone would benefit from the useSession hook having these features, both devs and all users of any site that has better-auth. Optimized out of the box without needing custom hooks on top of it. If there's no chance of this getting added to the library itself then I may just make a better-auth-swr package that can be used alongside it, but I feel like anyone using better-auth on a React based project definitely needs these features

<!-- gh-comment-id:2559320129 --> @daveycodez commented on GitHub (Dec 23, 2024): Yea I know I can write the hooks, but I was kind of hoping to see this built in since everyone would benefit from the useSession hook having these features, both devs and all users of any site that has better-auth. Optimized out of the box without needing custom hooks on top of it. If there's no chance of this getting added to the library itself then I may just make a better-auth-swr package that can be used alongside it, but I feel like anyone using better-auth on a React based project definitely needs these features
Author
Owner

@Bekacru commented on GitHub (Dec 23, 2024):

Oh yeah, the library idea would be great. And if we had a dedicated React package, this would definitely make sense. The only thing we do for each framework (Vue, React, Svelte...) is provide a very lightweight adapter. For example, for the client, we just call the nanostore atoms with the respective useStore hooks and nothing else. In the future, we may have a dedicated adapter, but for now, since we're keeping better-auth as a single package, we try to avoid framework-specific code as much as possible.

<!-- gh-comment-id:2559335648 --> @Bekacru commented on GitHub (Dec 23, 2024): Oh yeah, the library idea would be great. And if we had a dedicated React package, this would definitely make sense. The only thing we do for each framework (Vue, React, Svelte...) is provide a very lightweight adapter. For example, for the client, we just call the nanostore atoms with the respective `useStore` hooks and nothing else. In the future, we may have a dedicated adapter, but for now, since we're keeping `better-auth` as a single package, we try to avoid framework-specific code as much as possible.
Author
Owner

@Nicolab commented on GitHub (Dec 28, 2024):

Better-auth agnostic, it's great. I think also this should currently be handled by users (or another lib) rather than from better-auth lib.

I use Vue.js and React, the two have a different logic. If it can be useful to someone, here's how I do it with Vue.js:

watch(() => currentSession.value, (newVal) => {
    if (newVal.session.isPending || newVal.session.isRefetching) {
      appStore.user.value = null
      return
    }

    appStore.user.value = newVal?.user
}, { immediate: true })

By the way, from there, it's possible to synchronize the localStorage ;)

<!-- gh-comment-id:2564399970 --> @Nicolab commented on GitHub (Dec 28, 2024): Better-auth agnostic, it's great. I think also this should currently be handled by users (or another lib) rather than from better-auth lib. I use Vue.js and React, the two have a different logic. If it can be useful to someone, here's how I do it with Vue.js: ```ts watch(() => currentSession.value, (newVal) => { if (newVal.session.isPending || newVal.session.isRefetching) { appStore.user.value = null return } appStore.user.value = newVal?.user }, { immediate: true }) ``` By the way, from there, it's possible to synchronize the localStorage ;)
Author
Owner

@daveycodez commented on GitHub (Dec 28, 2024):

I don't think Vue uses the useSession React hook, the feature suggestion was for the React hook itself to reduce amount of "auth code" in the application. Plug and play with necessary features that every React app using better-auth would benefit from without having to re-write it manually and document it for every application. Yes we could all write our own logic to do these things. But we could also roll our own auth, right.

A feature like this is better to have built in than to Google through GitHub issues to figure out how to self implement, or just have a separate "official" better-auth-react library that might even be community managed. I think a lot of devs might not think of this issue even, so the users of those applications suffer the extra latency

<!-- gh-comment-id:2564426862 --> @daveycodez commented on GitHub (Dec 28, 2024): I don't think Vue uses the useSession React hook, the feature suggestion was for the React hook itself to reduce amount of "auth code" in the application. Plug and play with necessary features that every React app using better-auth would benefit from without having to re-write it manually and document it for every application. Yes we could all write our own logic to do these things. But we could also roll our own auth, right. A feature like this is better to have built in than to Google through GitHub issues to figure out how to self implement, or just have a separate "official" better-auth-react library that might even be community managed. I think a lot of devs might not think of this issue even, so the users of those applications suffer the extra latency
Author
Owner

@Nicolab commented on GitHub (Dec 29, 2024):

UseSession() is a Nanostore, not a React hook. So Vue.js use also useSession() provided by Better-auth.
Yes like you say, a different package for React would be relevant. That also open the door for Vue.js, Solid.js, Angular, htmx, Alpine, Astro, etc. So a list of external adapters is more clean, instead of bloat the main package with several useless dependencies.

<!-- gh-comment-id:2564784658 --> @Nicolab commented on GitHub (Dec 29, 2024): UseSession() is a Nanostore, not a React hook. So Vue.js use also useSession() provided by Better-auth. Yes like you say, a different package for React would be relevant. That also open the door for Vue.js, Solid.js, Angular, htmx, Alpine, Astro, etc. So a list of external adapters is more clean, instead of bloat the main package with several useless dependencies.
Author
Owner

@daveycodez commented on GitHub (Dec 29, 2024):

In this case isn't localStorage agnostic, and can be used within useSession as an option to benefit every framework. Just a simple option in createAuthClient options enableLocalStorageCache Boolean. This doesn't add any dependencies and should only be a few lines of code inside of useSession and/or getSession and will make all sites in every framework using better-auth snappier on initial render

<!-- gh-comment-id:2564819198 --> @daveycodez commented on GitHub (Dec 29, 2024): In this case isn't localStorage agnostic, and can be used within useSession as an option to benefit every framework. Just a simple option in createAuthClient options enableLocalStorageCache Boolean. This doesn't add any dependencies and should only be a few lines of code inside of useSession and/or getSession and will make all sites in every framework using better-auth snappier on initial render
Author
Owner

@reslear commented on GitHub (Dec 30, 2024):

yeah that would be cool for Offline First (pwa/capacitor) applications
also i found wrap solution af7e404900/packages/better-auth
but only for React but I think it could easily be adapted for vue.

<!-- gh-comment-id:2565438394 --> @reslear commented on GitHub (Dec 30, 2024): yeah that would be cool for Offline First (pwa/capacitor) applications also i found wrap solution https://github.com/onejs/one/tree/af7e4049000f60e41a7b231d56f16d0393f72abc/packages/better-auth but only for React but I think it could easily be adapted for vue.
Author
Owner

@daveycodez commented on GitHub (Feb 14, 2025):

@reslear

https://github.com/daveyplate/better-auth-tanstack

I use my Tanstack library with a persistClient for offline auth now, in Capacitor

<!-- gh-comment-id:2658167370 --> @daveycodez commented on GitHub (Feb 14, 2025): @reslear https://github.com/daveyplate/better-auth-tanstack I use my Tanstack library with a persistClient for offline auth now, in Capacitor
Author
Owner

@reslear commented on GitHub (Feb 14, 2025):

@daveycodez react 🤢

<!-- gh-comment-id:2658605314 --> @reslear commented on GitHub (Feb 14, 2025): @daveycodez react 🤢
Author
Owner

@joseph-9900 commented on GitHub (Mar 3, 2025):

@daveycodez Have you found any solution? Seeing the same result, on refresh the database is getting hit everytime! I have turned on cookieCache still

session: {
        expiresIn: 60 * 60 * 24 * 7, // 7 days
        updateAge: 60 * 60 * 24, // 1 day (every 1 day the session expiration is updated)
        cookieCache: {
            enabled: true,
            maxAge: 5 * 60 // Cache duration in seconds
        }
    },
<!-- gh-comment-id:2693250151 --> @joseph-9900 commented on GitHub (Mar 3, 2025): @daveycodez Have you found any solution? Seeing the same result, on refresh the database is getting hit everytime! I have turned on cookieCache still ``` session: { expiresIn: 60 * 60 * 24 * 7, // 7 days updateAge: 60 * 60 * 24, // 1 day (every 1 day the session expiration is updated) cookieCache: { enabled: true, maxAge: 5 * 60 // Cache duration in seconds } }, ```
Author
Owner

@Nikola-Milovic commented on GitHub (Apr 20, 2025):

Any news? I want to have offline capabilities and it would be beneficial to have session persist even if network is not available. Can this be solved via the plugin system?

<!-- gh-comment-id:2817040457 --> @Nikola-Milovic commented on GitHub (Apr 20, 2025): Any news? I want to have offline capabilities and it would be beneficial to have session persist even if network is not available. Can this be solved via the plugin system?
Author
Owner

@daveycodez commented on GitHub (Apr 20, 2025):

Any news? I want to have offline capabilities and it would be beneficial to have session persist even if network is not available. Can this be solved via the plugin system?

https://github.com/daveyplate/better-auth-tanstack

Use this with a TS Query Persist Client https://tanstack.com/query/latest/docs/framework/react/plugins/persistQueryClient

<!-- gh-comment-id:2817048657 --> @daveycodez commented on GitHub (Apr 20, 2025): > Any news? I want to have offline capabilities and it would be beneficial to have session persist even if network is not available. Can this be solved via the plugin system? https://github.com/daveyplate/better-auth-tanstack Use this with a TS Query Persist Client https://tanstack.com/query/latest/docs/framework/react/plugins/persistQueryClient
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#25858