T
TanStack17mo ago
other-emerald

Auth / beforeLoad / router.invalidate / redirect problem with router context update

i have the problem probably due to my understanding of data flow in router context? i have simple
async function onSubmit(data: z.infer<typeof FormSchema>) {
await auth.login(data).then(async () => {
await router.invalidate()
})
}
async function onSubmit(data: z.infer<typeof FormSchema>) {
await auth.login(data).then(async () => {
await router.invalidate()
})
}
to trigger beforeLoad after updating react query cache
import { createFileRoute, redirect } from '@tanstack/react-router'
import { z } from 'zod'

const fallback = '/' as const

export const Route = createFileRoute('/_auth')({
validateSearch: z.object({
redirect: z.string().optional().catch(''),
}),
beforeLoad: async ({ context, search }) => {
if (context.auth.isAuthenticated) {
throw redirect({ to: search.redirect || fallback })
}
},
staleTime: 0,
preloadStaleTime: 0,
})
import { createFileRoute, redirect } from '@tanstack/react-router'
import { z } from 'zod'

const fallback = '/' as const

export const Route = createFileRoute('/_auth')({
validateSearch: z.object({
redirect: z.string().optional().catch(''),
}),
beforeLoad: async ({ context, search }) => {
if (context.auth.isAuthenticated) {
throw redirect({ to: search.redirect || fallback })
}
},
staleTime: 0,
preloadStaleTime: 0,
})
and this is my authProvider
import axios from '@/utils/axios'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import React from 'react'

export type Auth = {
user: { email: string }
isAuthenticated: boolean
login: (user: { email: string }) => Promise<void>
logout: () => Promise<void>
isLoading: boolean
}

const AuthContext = React.createContext<Auth | null>(null)

export function AuthProvider({ children }: { children: React.ReactNode }) {
const queryClient = useQueryClient()

const mutation = useMutation({
mutationFn: (user: { email: string }) =>
axios.post('auth/login', user).then((res) => res.data),
onSuccess: (data) => {
queryClient.setQueryData(['user'], data)
return data
},
})

const { data: user, isLoading } = useQuery({
queryKey: ['user'],
queryFn: async () => await axios.get('api/v1/user').then((res) => res.data),
retry: false,
})

const isAuthenticated = !!user

const login = async (user: { email: string }) => {
await mutation.mutateAsync(user).then(async (data) => {
// await queryClient.invalidateQueries({ queryKey: ['user'] })
await queryClient.setQueryData(['user'], data)
})
}
console.log('user', user)

const logout = async () => {
await axios
.post('auth/logout')
.then(async () => {
await queryClient.setQueryData(['user'], null)
// await queryClient.invalidateQueries({ queryKey: ['user'] })
})
.catch(async () => {
await queryClient.setQueryData(['user'], null)
// await queryClient.invalidateQueries({ queryKey: ['user'] })
})
}

return (
<AuthContext.Provider
value={{ isAuthenticated, user, login, logout, isLoading }}
>
{children}
</AuthContext.Provider>
)
}

export function useAuth() {
const context = React.useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}
import axios from '@/utils/axios'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import React from 'react'

export type Auth = {
user: { email: string }
isAuthenticated: boolean
login: (user: { email: string }) => Promise<void>
logout: () => Promise<void>
isLoading: boolean
}

const AuthContext = React.createContext<Auth | null>(null)

export function AuthProvider({ children }: { children: React.ReactNode }) {
const queryClient = useQueryClient()

const mutation = useMutation({
mutationFn: (user: { email: string }) =>
axios.post('auth/login', user).then((res) => res.data),
onSuccess: (data) => {
queryClient.setQueryData(['user'], data)
return data
},
})

const { data: user, isLoading } = useQuery({
queryKey: ['user'],
queryFn: async () => await axios.get('api/v1/user').then((res) => res.data),
retry: false,
})

const isAuthenticated = !!user

const login = async (user: { email: string }) => {
await mutation.mutateAsync(user).then(async (data) => {
// await queryClient.invalidateQueries({ queryKey: ['user'] })
await queryClient.setQueryData(['user'], data)
})
}
console.log('user', user)

const logout = async () => {
await axios
.post('auth/logout')
.then(async () => {
await queryClient.setQueryData(['user'], null)
// await queryClient.invalidateQueries({ queryKey: ['user'] })
})
.catch(async () => {
await queryClient.setQueryData(['user'], null)
// await queryClient.invalidateQueries({ queryKey: ['user'] })
})
}

return (
<AuthContext.Provider
value={{ isAuthenticated, user, login, logout, isLoading }}
>
{children}
</AuthContext.Provider>
)
}

export function useAuth() {
const context = React.useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}
as you can see the context does not update before beforeLoad is checked again thanks to router.invalidate
6 Replies
other-emerald
other-emeraldOP17mo ago
i know i can add
useEffect(() => {
router.invalidate()
}, [auth, router])

function onSubmit(data: z.infer<typeof FormSchema>) {
auth.login(data)
}
useEffect(() => {
router.invalidate()
}, [auth, router])

function onSubmit(data: z.infer<typeof FormSchema>) {
auth.login(data)
}
but if possible I would like to know a way without using useEffect or bast way would be just to add
import { useAuth } from '@/hooks/useAuth'
import {
Outlet,
createFileRoute,
redirect,
useRouter,
} from '@tanstack/react-router'
import { useEffect } from 'react'

export const Route = createFileRoute('/_secure')({
beforeLoad: async ({ context, location }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: '/login',
search: {
redirect: location.href,
},
})
}
},
staleTime: 0,
preloadStaleTime: 0,
component: GuardWrapper,
})

function GuardWrapper() {
const router = useRouter()
const auth = useAuth()
useEffect(() => {
router.invalidate()
}, [auth, router])

return <Outlet />
}
import { useAuth } from '@/hooks/useAuth'
import {
Outlet,
createFileRoute,
redirect,
useRouter,
} from '@tanstack/react-router'
import { useEffect } from 'react'

export const Route = createFileRoute('/_secure')({
beforeLoad: async ({ context, location }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: '/login',
search: {
redirect: location.href,
},
})
}
},
staleTime: 0,
preloadStaleTime: 0,
component: GuardWrapper,
})

function GuardWrapper() {
const router = useRouter()
const auth = useAuth()
useEffect(() => {
router.invalidate()
}, [auth, router])

return <Outlet />
}
cos its working that way
other-emerald
other-emeraldOP17mo ago
conscious-sapphire
conscious-sapphire13mo ago
I have a similar issue with beforeLoad & AuthProvider after successful login of user the context.auth.isAuthenticated is still giving false in context of beforeLoad and the redirection is not happening to the page after login and stays on login forever
conscious-sapphire
conscious-sapphire13mo ago
@Borcio can you try this It fixed my issue
No description
multiple-amethyst
multiple-amethyst13mo ago
This is not a proper fix, this is a concurrency issue, your user is not available when beforeLoad is checked because the HTTP request for the user takes time, you need to wait for the request to finish before going forward and checking if the user is authenticated. What could fix this is a beforeLoad somewhere before your check if the user is authenticated that would wait for await queryClient.ensureQueryData(<user_query_options_here>) It should look like
- beforeLoad // Makes sure `user` is available
- beforeLoad // Checks authentication state
- beforeLoad // Makes sure `user` is available
- beforeLoad // Checks authentication state
these could be combined into a single beforeLoad too, that would first fetch/make sure user is available then check the authentication state
fascinating-indigo
fascinating-indigo12mo ago
do you guys fix this issue ? what are the solutions

Did you find this page helpful?