I created the skeleton of my app, with vite + react and supabase.
I set all the routes and created a context to understand if a user is logged in or not.
the context checks the localstorage if the tokens are there and if it finds them it should report to the path 'dashboard'.
once logged in everything works correctly, the problem is that when I refresh I am brought back to the login page even though the tokens are still saved in the localstorage.
I'm stuck and I can't solve it... could someone help me?
I tried to change the entire routing system several times but I don't understand where the error is.
I would like if the token is present in the localstorage you are redirected to the 'dashboard' path and not to the login after reloading the page
App.tsx
import {
createBrowserRouter,
Navigate,
Outlet,
RouterProvider,
useLocation,
} from "react-router-dom";
import { AuthContext } from "./context/AuthContext";
import { AuthProvider } from "./context/AuthContext";
import Login from "./pages/login/login";
import { useContext } from "react";
import Dashboard from "./pages/Dashboard/dashboard";
import { Sidebar } from "./components/custom/sidebar";
const PrivateRoutes = () => {
const location = useLocation();
const { tokens } = useContext(AuthContext);
return tokens.access_token !== null ? (
<div className="bg-background h-screen">
<div className="grid lg:grid-cols-5 h-screen">
<Sidebar className="hidden lg:flex lg:flex-col h-screen" />
<div className="col-span-3 lg:col-span-4 lg:border-l">
<div className="h-full px-4 py-6 lg:px-8">
<Outlet />
</div>
</div>
</div>
</div>
) : (
<Navigate to="/login" replace state={{ from: location }} />
);
};
const router = createBrowserRouter([
{
path: "/",
element: <PrivateRoutes />,
children: [
{
path: "dashboard",
element: <Dashboard />,
},
],
},
{
path: "/login",
element: <Login />,
},
]);
function App() {
return (
<>
<AuthProvider>
<RouterProvider router={router} />
</AuthProvider>
</>
);
}
export default App;
Login.tsx
import { useContext } from "react";
import { AuthContext } from "@/context/AuthContext";
import { cn } from "@/lib/utils";
import { useSignal } from "@preact/signals-react";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { ReloadIcon } from "@radix-ui/react-icons";
import { Toaster, toast } from "sonner";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { signIn, supabase } from "@/utils/supabase";
import { useNavigate } from "react-router-dom";
const formSchema = z.object({
email: z.string().email({
message: "Email address is not valid",
}),
password: z.string().min(8, {
message: "Password must be at least 8 characters.",
}),
});
export default function Login() {
const { setTokens } = useContext(AuthContext);
const isLoading = useSignal(false);
const navigate = useNavigate();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
});
const onSubmit = async (data: z.infer<typeof formSchema>) => {
isLoading.value = true;
const res = await signIn(data);
if (res.error) toast.error("Errore, credenziali errate");
else {
//const { from } = location.state || { from: { pathname: "/" } };
setTokens(res.data.session.access_token, res.data.session.refresh_token);
localStorage.setItem("access_token", res.data.session.access_token);
localStorage.setItem("refresh_token", res.data.session.access_token);
supabase.auth.setSession({ access_token: res.data.session.access_token, refresh_token: res.data.session.refresh_token });
navigate('/dashboard');
}
isLoading.value = false;
};
return (
<>
<div className="container h-screen relative hidden flex-col items-center justify-center md:grid lg:max-w-none lg:grid-cols-2 lg:px-0">
<Button asChild className={cn("absolute right-4 top-4 md:right-8 md:top-8")}>
<a href="auth">Register</a>
</Button>
<div className="relative hidden h-full flex-col bg-muted p-10 text-white dark:border-r lg:flex">
<div className="absolute inset-0 bg-zinc-900" />
<div className="relative z-20 flex items-center text-lg font-medium">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-6 w-6"
>
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
</svg>
Acme Inc
</div>
<div className="relative z-20 mt-auto">
<blockquote className="space-y-2">
<p className="text-lg">
“This library has saved me countless hours of work and
helped me deliver stunning designs to my clients faster than
ever before.”
</p>
<footer className="text-sm">Sofia Davis</footer>
</blockquote>
</div>
</div>
<div className="lg:p-8">
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[450px]">
<div className="flex flex-col space-y-2">
<h1 className="text-2xl font-semibold tracking-tight mb-2 text-center">
Login in your account
</h1>
</div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid gap-y-3"
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="Enter your email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
placeholder="Enter your password"
type="password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="pt-6 flex w-full">
<Button
className="w-full"
type="submit"
disabled={isLoading.value}
>
{isLoading.value === false ? (
<>
<span>Login</span>
</>
) : (
<>
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
please wait
</>
)}
</Button>
</div>
</form>
</Form>
</div>
</div>
</div>
<Toaster />
</>
);
}
AuthContext.tsx
import { createContext, useState, useEffect, ReactNode } from 'react';
interface Tokens {
access_token: string | null;
refresh_token: string | null;
}
interface AuthContextProps {
tokens: Tokens;
setTokens: (accessToken: string | null, refreshToken: string | null) => void;
}
const initialAuthContext: AuthContextProps = {
tokens: {
access_token: null,
refresh_token: null,
},
setTokens: () => {},
};
const AuthContext = createContext<AuthContextProps>(initialAuthContext);
interface AuthProviderProps {
children: ReactNode;
}
const AuthProvider = ({ children }: AuthProviderProps) => {
const [tokens, setTokens] = useState<Tokens>({
access_token: null,
refresh_token: null,
});
const setToken = (accessToken: string | null, refreshToken: string | null) => {
setTokens({ access_token: accessToken, refresh_token: refreshToken });
};
// Check for tokens in local storage on component mount
useEffect(() => {
const storedAccessToken = localStorage.getItem('access_token');
const storedRefreshToken = localStorage.getItem('refresh_token');
if (storedAccessToken && storedRefreshToken) {
setTokens({ access_token: storedAccessToken, refresh_token: storedRefreshToken });
}
console.log(tokens);
}, []);
return (
<AuthContext.Provider value={{ tokens, setTokens: setToken }}>
{children}
</AuthContext.Provider>
);
};
export { AuthContext, AuthProvider };
The AuthProvider
component uses the same initial state value as an unauthenticated user, so the redirection is immediately effected upon page reload. In other words, the AuthProvider
provides a null
token value on the initial render and PrivatesRoutes
renders the redirect to the login page.
Start from an indeterminant state, e.g. undefined
, and conditionally wait until the authentication status is confirmed/set in the auth context.
interface Tokens {
access_token: string | undefined| null;
refresh_token: string | undefined | null;
}
const AuthProvider = ({ children }: AuthProviderProps) => {
const [tokens, setTokens] = useState<Tokens>({
access_token: undefined,
refresh_token: undefined,
});
const setToken = (accessToken: string | null, refreshToken: string | null) => {
setTokens({ access_token: accessToken, refresh_token: refreshToken });
};
// Check for tokens in local storage on component mount
useEffect(() => {
const storedAccessToken = localStorage.getItem('access_token');
const storedRefreshToken = localStorage.getItem('refresh_token');
setTokens({
access_token: storedAccessToken ?? null,
refresh_token: storedRefreshToken ?? null
});
}, []);
return (
<AuthContext.Provider value={{ tokens, setTokens: setToken }}>
{children}
</AuthContext.Provider>
);
};
Update PrivateRoutes
to conditionally render null or some loading indicator while the auth status is confirmed/set.
const PrivateRoutes = () => {
const location = useLocation();
const { tokens } = useContext(AuthContext);
if (tokens.access_token === undefined) {
return null; // or loading indicator/spinner/etc
}
// tokens.access_token is either null or string at this point
return tokens.access_token ? (
<div className="bg-background h-screen">
<div className="grid lg:grid-cols-5 h-screen">
<Sidebar className="hidden lg:flex lg:flex-col h-screen" />
<div className="col-span-3 lg:col-span-4 lg:border-l">
<div className="h-full px-4 py-6 lg:px-8">
<Outlet />
</div>
</div>
</div>
</div>
) : (
<Navigate to="/login" replace state={{ from: location }} />
);
};
An alternative is to use a lazy initializer function to set the initial state value in lieu of the useEffect
hook. This will provide the correct initial state for the initial render cycle for the PrivateRoutes
component.
const AuthProvider = ({ children }: AuthProviderProps) => {
const [tokens, setTokens] = useState<Tokens>(() => {
const storedAccessToken = localStorage.getItem('access_token');
const storedRefreshToken = localStorage.getItem('refresh_token');
return {
access_token: storedAccessToken ?? null,
refresh_token: storedRefreshToken ?? null
};
});
const setToken = (accessToken: string | null, refreshToken: string | null) => {
setTokens({ access_token: accessToken, refresh_token: refreshToken });
};
return (
<AuthContext.Provider value={{ tokens, setTokens: setToken }}>
{children}
</AuthContext.Provider>
);
};