I am currently building an app using following stack: Next.js Node.js, Express.js PostgreSQL.
I build an authentication using JWT HTTPOnly cookie. Below is my code to how I do login, logout and verify if a user is logged in or not.
In the frontend I am using RTK Query in nextjs to handle api Queries.
I create a SessionProvider component and wrapped around children in the layout file so when user first load page this component checks if a user is logged in or not and if yes it updates the react context.
"use client"
import { UserI } from '@/interfaces';
import { useVerifyQuery } from '@/lib/features/authApi';
import { createContext, PropsWithChildren } from 'react';
interface ContextI {
user: UserI | null;
isAuthenticated: boolean;
error: string | null;
isLoading: boolean;
}
export const SessionContext = createContext<ContextI>({
user: null,
isAuthenticated: false,
error: null,
isLoading: false
});
export const SessionProvider = ({ children }: PropsWithChildren) => {
const { data, isLoading } = useVerifyQuery();
const user = data?.data;
const error = data?.error;
if (error) console.log(error);
return (
<SessionContext.Provider value={{ user: user ?? null, isAuthenticated: user ? true : false, error: error ?? null, isLoading }}>
{children}
</SessionContext.Provider>
);
};
I use the react context in following way to check if a user is authenticated or not and if yes I update the navlinks in similar way
"use client"
import { SessionContext } from '@/app/SessionProvider';
import { ButtonGroup, Flex, HStack, Spinner } from '@chakra-ui/react';
import { usePathname } from 'next/navigation';
import { useContext } from 'react';
import LinkBtn from './LinkBtn';
import Logout from './Logout';
const NavLinks = () => {
const { user, isAuthenticated, isLoading } = useContext(SessionContext);
const currPath = usePathname();
if (isLoading) return <Spinner />
return (
<Flex className='gap-3 justify-center items-center'>
{isAuthenticated ?
<>
<HStack>
<LinkBtn href={currPath === '/' ? '/dashboard' : '/'}>
{currPath === '/' ? 'Dashboard' : 'Home'}
</LinkBtn>
<Logout name={user?.name} />
</HStack>
</>
:
<ButtonGroup>
<LinkBtn href='/user/signup'>Sign up</LinkBtn>
<LinkBtn href='/user/login'>Login</LinkBtn>
</ButtonGroup>
}
</Flex>
)
}
export default NavLinks
Below is my login component
"use client"
import { handleErrors } from '@/components/error/handleErrors'
import { toast } from "@/components/error/Toast"
import Auth from '@/components/ui/Auth'
import Btn from '@/components/ui/Btn'
import ErrorMsg from '@/components/ui/ErrorMsg'
import LinkBtn from '@/components/ui/LinkBtn'
import Logo from '@/components/ui/Logo'
import { unexpectedError } from '@/constants'
import { AuthI } from '@/interfaces'
import { useLoginMutation } from '@/lib/features/authApi'
import { validateLogin } from '@/validation'
import { ButtonGroup, Input, Spinner } from '@chakra-ui/react'
import { zodResolver } from '@hookform/resolvers/zod'
import { useRouter } from 'next/navigation'
import { useForm } from "react-hook-form"
const Login = () => {
const { register, handleSubmit, formState: { errors } } = useForm<AuthI>({
resolver: zodResolver(validateLogin)
});
const navigation = useRouter();
const [login, { isLoading, error }] = useLoginMutation();
if (error) handleErrors(error, unexpectedError.type);
const onSubmit = async (data: AuthI) => {
try {
await login(data);
navigation.push("/");
} catch (error) {
toast.error(unexpectedError.message, { toastId: unexpectedError.type });
console.log(error);
}
};
return (
<Auth>
<form className='flex flex-col gap-2 p-10' onSubmit={handleSubmit(onSubmit)}>
<Logo />
{errors && <ErrorMsg>{errors.email?.message}</ErrorMsg>}
<Input type='email' isRequired placeholder='Enter your email...' {...register("email")} />
{errors && <ErrorMsg>{errors.password?.message}</ErrorMsg>}
<Input type='password' isRequired placeholder='Enter your password...' {...register("password")} />
<ButtonGroup my={2}>
<Btn type='submit' isDisabled={isLoading}>
Login {isLoading && <Spinner ml={1} size='sm' />}
</Btn>
<LinkBtn href='/'>
Cancel
</LinkBtn>
</ButtonGroup>
</form>
</Auth>
)
}
export default Login
The issue i am facing is whenever I do login or logout react context does not update state until I REFRESH my page how can ensure the state is updated changes are pushed. I am completely blank on how to do fix it. PLEASE HELP!
THANK YOU FOR LOOKING INTO THIS.
I tried to look for different ways to solve this like useEffect, useState, I tried to change react context into redux slice but I cannot find a way to fix this.
Thank you everone for looking into this. I finally found the solution. Here is the code. I just needed to use hooks.
"use client"
import { ContextI } from '@/interfaces';
import { useVerifyQuery } from '@/lib/features/authApi';
import { createContext, PropsWithChildren, useEffect, useState } from 'react';
const initialContext = {
user: null,
isAuthenticated: false,
error: null,
isLoading: false,
} as ContextI;
export const SessionContext = createContext<ContextI>(initialContext);
export const SessionProvider = ({ children }: PropsWithChildren) => {
const { data, isLoading } = useVerifyQuery();
const user = data?.data ?? null;
const error = data?.error ?? null;
const [context, setContext] = useState<ContextI>({
user,
isAuthenticated: !!data?.data,
error,
isLoading,
updateContext: (newContext: Partial<ContextI>) => {
setContext((prevContext) => ({
...prevContext,
...newContext,
}));
}
});
useEffect(() => {
setContext((prevContext) => ({
...prevContext,
user,
isAuthenticated: !!user,
error,
isLoading
}));
}, [data, isLoading]);
return (
<SessionContext.Provider value={{ ...context }}>
{children}
</SessionContext.Provider>
);
};