T
TanStack•2mo ago
adverse-sapphire

Where should I put supabase.auth.onAuthStateChange?

Hi, I'm trying to implement supabase.auth.onAuthStateChange into the client since supabase does not recommend using getSession() in the BE and getUser() does not include the custom claims in the token. To implement the subscription, is there any recommended pattern? Right now I can think of 1) Using React Context - I already implemented this for ThemeProvider (shadcn). Then I wrapped the context around children in RootDocument.
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
<HeadContent />
</head>
<body>
<ThemeProvider>{children}</ThemeProvider>
{/* <TanStackRouterDevtools position="bottom-right" /> */}
<Scripts />
</body>
</html>
);
}
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
<HeadContent />
</head>
<body>
<ThemeProvider>{children}</ThemeProvider>
{/* <TanStackRouterDevtools position="bottom-right" /> */}
<Scripts />
</body>
</html>
);
}
Is there any other potential ways? I'm not sure how we can implement subscription in tanstack start router - i'm not sure if we can implement like React Context/Hooks in Router
Router Context | TanStack Router React Docs
TanStack Router's router context is a very powerful tool that can be used for dependency injection among many other things. Aptly named, the router context is passed through the router and down throug...
19 Replies
adverse-sapphire
adverse-sapphireOP•2mo ago
I'm asking this because I would like to use the user/session info inside beforeload so I can redirect user if they are not authenticated. With react context, I'm not sure how to implement it. I think if I don't use react context, then I will use tanstack query + fetch session so at least i can cache the info
deep-jade
deep-jade•2mo ago
You can add it inside main.tsx, and pass it as context to the router provider But this would be ok only for frontend. Like, for react router. Not sure if it will be ok for start
adverse-sapphire
adverse-sapphireOP•2mo ago
I found a way to wrap ReactContext around. It is sorta like what Mihai said above. What I did: - Create client.tsx so I can override the client entry - doc
// src/client.tsx
import { RouterProvider } from '@tanstack/react-router';
import { StrictMode } from 'react';
import { hydrateRoot } from 'react-dom/client';
import { AuthProvider, useAuth } from './components/AuthProvider';
import { createRouter } from './router';

const router = createRouter();

function InnerApp() {
const user = useAuth();
return <RouterProvider router={router} context={{ user }} />;
}

function App() {
return (
<AuthProvider>
<InnerApp />
</AuthProvider>
);
}

hydrateRoot(
document,
<StrictMode>
<App />
</StrictMode>
);
// src/client.tsx
import { RouterProvider } from '@tanstack/react-router';
import { StrictMode } from 'react';
import { hydrateRoot } from 'react-dom/client';
import { AuthProvider, useAuth } from './components/AuthProvider';
import { createRouter } from './router';

const router = createRouter();

function InnerApp() {
const user = useAuth();
return <RouterProvider router={router} context={{ user }} />;
}

function App() {
return (
<AuthProvider>
<InnerApp />
</AuthProvider>
);
}

hydrateRoot(
document,
<StrictMode>
<App />
</StrictMode>
);
- create normal React Context stuff like useAuth and AuthProvider Note: This is my current router setup ( i disabled ssr)
export function createRouter() {
const queryClient = new QueryClient();
const router = routerWithQueryClient(
createTanStackRouter({
routeTree,
scrollRestoration: true,
defaultSsr: false,
context: {
queryClient,
user: {
isAuthenticated: false,
},
},
defaultErrorComponent: DefaultCatchBoundary,
defaultNotFoundComponent: () => <NotFound />,
}),
queryClient
);

return router;
}
export function createRouter() {
const queryClient = new QueryClient();
const router = routerWithQueryClient(
createTanStackRouter({
routeTree,
scrollRestoration: true,
defaultSsr: false,
context: {
queryClient,
user: {
isAuthenticated: false,
},
},
defaultErrorComponent: DefaultCatchBoundary,
defaultNotFoundComponent: () => <NotFound />,
}),
queryClient
);

return router;
}
Learn the Basics | TanStack Start React Docs
This guide will help you learn the basics behind how TanStack Start works, regardless of how you set up your project. Dependencies TanStack Start is powered by and . TanStack Router: A router for buil...
deep-jade
deep-jade•2mo ago
Yea. Exactly like that!
adverse-sapphire
adverse-sapphireOP•2mo ago
But it looks like im having HTML rendered mismatch which points to the RouterProvider My AuthProvider
const AuthProviderContext = createContext<User>({ isAuthenticated: false });

type AuthProviderProps = {
children: ReactNode;
};

export function AuthProvider({ children }: AuthProviderProps) {
const [user, setUser] = useState<User>({ isAuthenticated: false });

useEffect(() => {
const supabase = getBrowserClient();
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => {
/**
* Useful doc for this subscription https://supabase.com/docs/reference/javascript/auth-onauthstatechange
*/
console.log('Event:', event);
console.log('Session: ', session);

if (event === 'INITIAL_SESSION') {
// if (session) {
// setUser(getUserFromSession(session));
// }
} else if (event === 'SIGNED_IN') {
// handle sign-in
} else if (event === 'SIGNED_OUT') {
// handle sign-out
} else if (event === 'TOKEN_REFRESHED') {
// update session claims maybe
} else if (event === 'USER_UPDATED') {
// update user data
}
});

return () => {
subscription.unsubscribe();
};
}, []);

return (
<AuthProviderContext.Provider value={user}>
{children}
</AuthProviderContext.Provider>
);
}

function getUserFromSession(session: Session) {
const jwt = jwtDecode<JwtClaims>(session.access_token);

const user: User = {
isAuthenticated: true,
plan: jwt.plan,
};

return user;
}

export const useAuth = () => {
const context = useContext(AuthProviderContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
const AuthProviderContext = createContext<User>({ isAuthenticated: false });

type AuthProviderProps = {
children: ReactNode;
};

export function AuthProvider({ children }: AuthProviderProps) {
const [user, setUser] = useState<User>({ isAuthenticated: false });

useEffect(() => {
const supabase = getBrowserClient();
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => {
/**
* Useful doc for this subscription https://supabase.com/docs/reference/javascript/auth-onauthstatechange
*/
console.log('Event:', event);
console.log('Session: ', session);

if (event === 'INITIAL_SESSION') {
// if (session) {
// setUser(getUserFromSession(session));
// }
} else if (event === 'SIGNED_IN') {
// handle sign-in
} else if (event === 'SIGNED_OUT') {
// handle sign-out
} else if (event === 'TOKEN_REFRESHED') {
// update session claims maybe
} else if (event === 'USER_UPDATED') {
// update user data
}
});

return () => {
subscription.unsubscribe();
};
}, []);

return (
<AuthProviderContext.Provider value={user}>
{children}
</AuthProviderContext.Provider>
);
}

function getUserFromSession(session: Session) {
const jwt = jwtDecode<JwtClaims>(session.access_token);

const user: User = {
isAuthenticated: true,
plan: jwt.plan,
};

return user;
}

export const useAuth = () => {
const context = useContext(AuthProviderContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
maybe because I try to override the user when thing loads 🤔
equal-aqua
equal-aqua•2mo ago
you need to use <StartClient> otherwise router wont hydrate but it should still be possible to implement this if you wrap both server and client entrypoints with the react provider
adverse-sapphire
adverse-sapphireOP•2mo ago
I have defaultSsr:false tho, does that mean I wouldn't need server entrypoints?
equal-aqua
equal-aqua•2mo ago
see other thread please use one thread
adverse-sapphire
adverse-sapphireOP•2mo ago
I'll ask question on this one since the code snippet is above
you cannot just render "nothing" on the server
Could you give an example of how server entrypoint file may look like with React provider?
equal-aqua
equal-aqua•2mo ago
this is the default packages/react-start-server/src/defaultStreamHandler.tsx
import {
defineHandlerCallback,
renderRouterToStream,
} from '@tanstack/react-router/ssr/server'
import { StartServer } from './StartServer'

export const defaultStreamHandler = defineHandlerCallback(
({ request, router, responseHeaders }) =>
renderRouterToStream({
request,
router,
responseHeaders,
children: <StartServer router={router} />,
}),
)
import {
defineHandlerCallback,
renderRouterToStream,
} from '@tanstack/react-router/ssr/server'
import { StartServer } from './StartServer'

export const defaultStreamHandler = defineHandlerCallback(
({ request, router, responseHeaders }) =>
renderRouterToStream({
request,
router,
responseHeaders,
children: <StartServer router={router} />,
}),
)
you would just wrap StartServer however, it does not allow passing in context yet but since StartServer is just this
export function StartServer<TRouter extends AnyRouter>(props: {
router: TRouter
}) {
return <RouterProvider router={props.router} />
}
export function StartServer<TRouter extends AnyRouter>(props: {
router: TRouter
}) {
return <RouterProvider router={props.router} />
}
you could just use RouterProviderinstead for now to experiment with it
adverse-sapphire
adverse-sapphireOP•2mo ago
Gotcha, thank you! I'll use RouterProvider to experiment for now and hopefully solve the rendered HTML mismatch Are we planning to allow passing context into <Start*/>?
equal-aqua
equal-aqua•2mo ago
maybe? depends on what you find out 😉 btw, on the client, you can just build you own startclient for now based on the actual impl
import { Await, RouterProvider } from '@tanstack/react-router'
import { hydrate } from '@tanstack/start-client-core'
import type { AnyRouter } from '@tanstack/router-core'

let hydrationPromise: Promise<void | Array<Array<void>>> | undefined

export function StartClient(props: { router: AnyRouter }) {
if (!hydrationPromise) {
if (!props.router.state.matches.length) {
hydrationPromise = hydrate(props.router)
} else {
hydrationPromise = Promise.resolve()
}
}
return (
<Await
promise={hydrationPromise}
children={() => <RouterProvider router={props.router} />}
/>
)
}
import { Await, RouterProvider } from '@tanstack/react-router'
import { hydrate } from '@tanstack/start-client-core'
import type { AnyRouter } from '@tanstack/router-core'

let hydrationPromise: Promise<void | Array<Array<void>>> | undefined

export function StartClient(props: { router: AnyRouter }) {
if (!hydrationPromise) {
if (!props.router.state.matches.length) {
hydrationPromise = hydrate(props.router)
} else {
hydrationPromise = Promise.resolve()
}
}
return (
<Await
promise={hydrationPromise}
children={() => <RouterProvider router={props.router} />}
/>
)
}
adverse-sapphire
adverse-sapphireOP•2mo ago
Hahaha thank you! I never thought about looking at the packages to see the implementation and then tweak it. Lemme give it a shot!!
foreign-sapphire
foreign-sapphire•2mo ago
I had hydration mismatch issue yesterday, after updating to the latest packages, I fixed it by adding react vite plugin outside of the tanstackstart plugin, and enabled the customreactplugin inside tanstackstart, the error was gone maybe it might be helpful
import path from "node:path";
import tailwindcss from "@tailwindcss/vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import tsConfigPaths from "vite-tsconfig-paths";
export default defineConfig({
server: {
port: 5173,
},
plugins: [
tsConfigPaths({
projects: ["./tsconfig.json"],
}),
tanstackStart({
spa: {
enabled: true,
},
customViteReactPlugin: true,
target: "cloudflare-module",
}),
tailwindcss(),
viteReact(),
],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});
import path from "node:path";
import tailwindcss from "@tailwindcss/vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import tsConfigPaths from "vite-tsconfig-paths";
export default defineConfig({
server: {
port: 5173,
},
plugins: [
tsConfigPaths({
projects: ["./tsconfig.json"],
}),
tanstackStart({
spa: {
enabled: true,
},
customViteReactPlugin: true,
target: "cloudflare-module",
}),
tailwindcss(),
viteReact(),
],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});
adverse-sapphire
adverse-sapphireOP•2mo ago
I tried both attempts but they have a problem on their own: #1 attempted replacing StartServer like:
//server.tsx
function InnerApp({ router }: { router: AnyRouter }) {
const user = useAuth();
return <RouterProvider router={router} context={{ user }} />;
}

function App({ router }: { router: AnyRouter }) {
return (
<AuthProvider>
<InnerApp router={router} />
</AuthProvider>
);
}

export const defaultStreamHandler = defineHandlerCallback(
({ request, router, responseHeaders }) =>
renderRouterToStream({
request,
router,
responseHeaders,
children: <App router={router} />,
})
);

export default createStartHandler({
createRouter,
})(defaultStreamHandler);
//server.tsx
function InnerApp({ router }: { router: AnyRouter }) {
const user = useAuth();
return <RouterProvider router={router} context={{ user }} />;
}

function App({ router }: { router: AnyRouter }) {
return (
<AuthProvider>
<InnerApp router={router} />
</AuthProvider>
);
}

export const defaultStreamHandler = defineHandlerCallback(
({ request, router, responseHeaders }) =>
renderRouterToStream({
request,
router,
responseHeaders,
children: <App router={router} />,
})
);

export default createStartHandler({
createRouter,
})(defaultStreamHandler);
and client.tsx
// client.tsx
const router = createRouter();

function InnerApp() {
const user = useAuth();
return <RouterProvider router={router} context={{ user }} />;
}

function App() {
return (
<AuthProvider>
<InnerApp />
</AuthProvider>
);
}

hydrateRoot(
document,
<StrictMode>
<App />
</StrictMode>
);
// client.tsx
const router = createRouter();

function InnerApp() {
const user = useAuth();
return <RouterProvider router={router} context={{ user }} />;
}

function App() {
return (
<AuthProvider>
<InnerApp />
</AuthProvider>
);
}

hydrateRoot(
document,
<StrictMode>
<App />
</StrictMode>
);
-> It still has the hydration issue :( #2 Implement custom StartClient with/without modifying the server entry:
let hydrationPromise: Promise<void | Array<Array<void>>> | undefined;

/**
* Custom implementation of StartClient from react-start-client so I can pass in the context to RouterProvider
*/
export function StartClient(props: {
router: AnyRouter;
context: { user: User };
}) {
if (!hydrationPromise) {
if (!props.router.state.matches.length) {
hydrationPromise = hydrate(props.router);
} else {
hydrationPromise = Promise.resolve();
}
}
return (
<Await
promise={hydrationPromise}
children={() => (
<RouterProvider router={props.router} context={props.context} />
)}
/>
);
}

const router = createRouter();

function InnerApp() {
const user = useAuth();
return <StartClient router={router} context={{ user }} />;
}

function App() {
return (
<AuthProvider>
<InnerApp />
</AuthProvider>
);
}

hydrateRoot(
document,
<StrictMode>
<App />
</StrictMode>
);
let hydrationPromise: Promise<void | Array<Array<void>>> | undefined;

/**
* Custom implementation of StartClient from react-start-client so I can pass in the context to RouterProvider
*/
export function StartClient(props: {
router: AnyRouter;
context: { user: User };
}) {
if (!hydrationPromise) {
if (!props.router.state.matches.length) {
hydrationPromise = hydrate(props.router);
} else {
hydrationPromise = Promise.resolve();
}
}
return (
<Await
promise={hydrationPromise}
children={() => (
<RouterProvider router={props.router} context={props.context} />
)}
/>
);
}

const router = createRouter();

function InnerApp() {
const user = useAuth();
return <StartClient router={router} context={{ user }} />;
}

function App() {
return (
<AuthProvider>
<InnerApp />
</AuthProvider>
);
}

hydrateRoot(
document,
<StrictMode>
<App />
</StrictMode>
);
--> Context updating takes too long to update, so when beforeLoad check was bypassed. Funny thing is, when i have the hydration issue, the context was provided fast enough so the beforeLoad was not bypassed. For context, for authenticated routes what I did was:
beforeLoad: ({ context, search }) => {
if (context.user.isAuthenticated) {
throw redirect({
to: search?.redirectUrl || '/',
});
}
},
beforeLoad: ({ context, search }) => {
if (context.user.isAuthenticated) {
throw redirect({
to: search?.redirectUrl || '/',
});
}
},
which navigates user to login and in login check again if user isAuthenticated then renavigate them again. Without custom StartClient, this one works but ran into mismatch HTML rendered Not sure which tanstack start version you are on, but I couldn't find the customReactPlugin on mine 😦 Mine is
"@tanstack/react-query": "^5.81.5",
"@tanstack/react-router": "^1.125.6",
"@tanstack/react-router-devtools": "^1.125.6",
"@tanstack/react-router-with-query": "^1.125.1",
"@tanstack/react-start": "^1.125.6",
"@tanstack/react-query": "^5.81.5",
"@tanstack/react-router": "^1.125.6",
"@tanstack/react-router-devtools": "^1.125.6",
"@tanstack/react-router-with-query": "^1.125.1",
"@tanstack/react-start": "^1.125.6",
adverse-sapphire
adverse-sapphireOP•2mo ago
The order of the execution is like this
No description
foreign-sapphire
foreign-sapphire•2mo ago
maybe you should try updating the packages then, mine is like this:
"@tanstack/react-form": "^1.14.1",
"@tanstack/react-router": "^1.127.0",
"@tanstack/react-router-devtools": "^1.127.0",
"@tanstack/react-start": "^1.127.0",
"@tanstack/react-store": "^0.7.3",
"@tanstack/react-form": "^1.14.1",
"@tanstack/react-router": "^1.127.0",
"@tanstack/react-router-devtools": "^1.127.0",
"@tanstack/react-start": "^1.127.0",
"@tanstack/react-store": "^0.7.3",
adverse-sapphire
adverse-sapphireOP•2mo ago
I didn't realise 1.127 is out Updated the package and got rid of the React context/Start client etc -> no more issue
foreign-sapphire
foreign-sapphire•2mo ago
I successfully implemented a context for my usecase, with custom start client, the catch is, on dev mode, even if my app is on spa mode, it is rendering things on the server, on first load, i think the loader functions and stuff, here is my implementation:
//router.tsx
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
import { DefaultCatchBoundary } from "./components/DefaultCatchBoundary";
import { NotFound } from "./components/NotFound";

import { routeTree } from "./routeTree.gen";

export function createRouter() {
const router = createTanStackRouter({
routeTree,
context: {
// custom function for the unwanted SSR
// @ts-ignore
t: (key: string) => key,
},
defaultPreload: "intent",
defaultErrorComponent: DefaultCatchBoundary,
defaultNotFoundComponent: () => <NotFound />,
scrollRestoration: true,
});

return router;
}

declare module "@tanstack/react-router" {
interface Register {
router: ReturnType<typeof createRouter>;
}
}
//router.tsx
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
import { DefaultCatchBoundary } from "./components/DefaultCatchBoundary";
import { NotFound } from "./components/NotFound";

import { routeTree } from "./routeTree.gen";

export function createRouter() {
const router = createTanStackRouter({
routeTree,
context: {
// custom function for the unwanted SSR
// @ts-ignore
t: (key: string) => key,
},
defaultPreload: "intent",
defaultErrorComponent: DefaultCatchBoundary,
defaultNotFoundComponent: () => <NotFound />,
scrollRestoration: true,
});

return router;
}

declare module "@tanstack/react-router" {
interface Register {
router: ReturnType<typeof createRouter>;
}
}
here is the custom startClient component:
import { Await, RouterProvider } from "@tanstack/react-router";
import type { AnyContext, AnyRouter } from "@tanstack/router-core";
import { hydrate } from "@tanstack/start-client-core";

let hydrationPromise: Promise<void | Array<Array<void>>> | undefined;

export function StartClient(props: {
router: AnyRouter;
context?: AnyContext;
}) {
if (!hydrationPromise) {
if (!props.router.state.matches.length) {
hydrationPromise = hydrate(props.router);
} else {
hydrationPromise = Promise.resolve();
}
}
return (
<Await
promise={hydrationPromise}
children={() => (
<RouterProvider router={props.router} context={props.context} />
)}
/>
);
}
import { Await, RouterProvider } from "@tanstack/react-router";
import type { AnyContext, AnyRouter } from "@tanstack/router-core";
import { hydrate } from "@tanstack/start-client-core";

let hydrationPromise: Promise<void | Array<Array<void>>> | undefined;

export function StartClient(props: {
router: AnyRouter;
context?: AnyContext;
}) {
if (!hydrationPromise) {
if (!props.router.state.matches.length) {
hydrationPromise = hydrate(props.router);
} else {
hydrationPromise = Promise.resolve();
}
}
return (
<Await
promise={hydrationPromise}
children={() => (
<RouterProvider router={props.router} context={props.context} />
)}
/>
);
}
here is my client.tsx file:
// src/client.tsx

import { useStore } from "@tanstack/react-store";
import { StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { IntlProvider, useTranslations } from "use-intl";
import { languages } from "./lib/constants";
import { languageStore } from "./lib/i18n";
import { createRouter } from "./router";
import { StartClient } from "./start";

const router = createRouter();

function ClientWithContext() {
console.log("sup");
const t = useTranslations();
return <StartClient router={router} context={{ t }} />;
}

function AppWithIntl({ children }: { children: any }) {
const currentLocale = useStore(languageStore, (state) => state.currentLocale);
const language =
languages.find((l) => l.code === currentLocale) || languages[0];

return (
<IntlProvider
locale={language.code}
messages={language.messages}
now={new Date()}
timeZone="Asia/Tashkent"
formats={{
dateTime: {
default: {
day: "numeric",
month: "long",
year: "numeric",
hour: "numeric",
minute: "numeric",
},
},
}}
>
{children}
</IntlProvider>
);
}

hydrateRoot(
document,
<StrictMode>
<AppWithIntl>
<ClientWithContext />
</AppWithIntl>
</StrictMode>,
);
// src/client.tsx

import { useStore } from "@tanstack/react-store";
import { StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { IntlProvider, useTranslations } from "use-intl";
import { languages } from "./lib/constants";
import { languageStore } from "./lib/i18n";
import { createRouter } from "./router";
import { StartClient } from "./start";

const router = createRouter();

function ClientWithContext() {
console.log("sup");
const t = useTranslations();
return <StartClient router={router} context={{ t }} />;
}

function AppWithIntl({ children }: { children: any }) {
const currentLocale = useStore(languageStore, (state) => state.currentLocale);
const language =
languages.find((l) => l.code === currentLocale) || languages[0];

return (
<IntlProvider
locale={language.code}
messages={language.messages}
now={new Date()}
timeZone="Asia/Tashkent"
formats={{
dateTime: {
default: {
day: "numeric",
month: "long",
year: "numeric",
hour: "numeric",
minute: "numeric",
},
},
}}
>
{children}
</IntlProvider>
);
}

hydrateRoot(
document,
<StrictMode>
<AppWithIntl>
<ClientWithContext />
</AppWithIntl>
</StrictMode>,
);
and on __root.tsx file:
const t = createTranslator({ locale: "en", messages });

interface RouterContext {
t: typeof t;
}
export const Route = createRootRouteWithContext<RouterContext>()({ ...})
const t = createTranslator({ locale: "en", messages });

interface RouterContext {
t: typeof t;
}
export const Route = createRootRouteWithContext<RouterContext>()({ ...})
now i can do like this:
export const Route = createFileRoute("/dashboard")({
beforeLoad: () => {
if (!profileStore.state) {
throw redirect({ to: "/login" });
}
},
loader: ({ context }) => {
return {
crumb: context.t("dashboard"),
};
},
component: DashboardLayout,
});
export const Route = createFileRoute("/dashboard")({
beforeLoad: () => {
if (!profileStore.state) {
throw redirect({ to: "/login" });
}
},
loader: ({ context }) => {
return {
crumb: context.t("dashboard"),
};
},
component: DashboardLayout,
});
for your case, if your code is fully client side, i'd just use tanstack store, it works even in the beforelaod as you can see in my beforeload function, and in your top level root component, you can update it using useEffect or something

Did you find this page helpful?