Search code examples
node.jsauthenticationnext.jsreact-contextcookie-httponly

Update react context without refreshing page on state update


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.


Solution

  • 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>
        );
    };