I am working with the T3 Stack and got stuck creating an AuthGuard. This AuthGuard essentially acts as a Page Manager
that redirects the user to the appropriate page.
I have set up a working version, but I am seeing ways to reduce redirects, add loading screens, and minimize screen flashing.
The SessionContext
calls the database to fetch user information, such as schemes and roles.
SessionProvider
is wrapped around AuthGuard
Given this, notFound()
is displayed for a split second (in cases where page is not found), then the login is shown and then the redirected to Home or else login.
How can I improve this without using middleware.ts or other 3rd party libraries?
"use client";
import { PropsWithChildren, useContext, useEffect, useState } from "react";
import { SessionContext } from "./SessionContext";
import { usePathname, useRouter } from "next/navigation";
const PUBLIC_ROUTES = ['/login', '/signup'];
export const AuthGuard: React.FC<PropsWithChildren> = ({ children }) => {
  const context = useContext(SessionContext);
  const user = context?.user;
  const loading = context?.loading;
  const error = context?.error;
  const pathname = usePathname();
  const router = useRouter();
  const [hasCheckedAuth, setHasCheckedAuth] = useState(false);
useEffect(() => {
    if (!loading) {
      if (!user && !PUBLIC_ROUTES.includes(pathname)) {
        router.replace('/login');
      } else if (user && PUBLIC_ROUTES.includes(pathname)) {
        router.replace('/');
      } else {
        setHasCheckedAuth(true);
      }
    }
  }, [user, loading, pathname]);
if (loading || !hasCheckedAuth) {
    return <LoadingSpinner />;
  }
if (error) {
    return <div>Error: {error.message}</div>;
  }
return <>{children}</>;
};
const LoadingSpinner: React.FC = () => (
  <div className="flex justify-center items-center h-screen">
    <div className="animate-spin rounded-full h-32 w-32 border-t-2 border-b-2 border-gray-900"></div>
  </div>
);
SessionContext.tsx
"use client";
import { createContext, PropsWithChildren, useCallback, useEffect, useMemo, useState } from "react";
import { SessionUser } from "~/types/User";
import { useFirebaseSession } from "./hooks/useFirebaseSession";
import { api } from "~/trpc/react";
interface SessionContextType {
user: SessionUser | null;
loading: boolean;
error: Error | null;
}
export const SessionContext = createContext<SessionContextType | undefined>(undefined);
export const SessionProvider: React.FC<PropsWithChildren> = ({ children }) => {
const { user: firebaseUser, loading: firebaseLoading, error: firebaseError } = useFirebaseSession();
const [sessionUser, setSessionUser] = useState<SessionUser | null>(null);
const {
data: dbUser,
error: dbError,
isLoading: dbLoading,
refetch: refetchDbUser
} = api.user.getSessionUser.useQuery(
undefined,
{
enabled: !!firebaseUser,
}
);
useEffect(() => {
if(dbUser && firebaseUser) {
setSessionUser({
...firebaseUser,
// Add Database User Data
})
} else {
setSessionUser(null);
}
}, [dbUser, firebaseUser]);
useEffect(() => {
if(firebaseUser) {
void refetchDbUser();
} else {
setSessionUser(null);
}
}, [firebaseUser, refetchDbUser]);
const value = useMemo(() => {
return {
user: sessionUser,
error: dbError instanceof Error ? dbError : firebaseError ?? null,
loading: firebaseLoading ?? dbLoading
}
}, [sessionUser, dbError, firebaseError, firebaseLoading, dbLoading])
return (
<SessionContext.Provider value={value}>{children}</SessionContext.Provider >
)
}