You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在登录/登出后无需刷新页面更新React Context状态?

问题:登录/登出后React Context状态无法即时更新

技术栈

  • Next.js
  • Node.js + Express.js
  • PostgreSQL
  • JWT HTTPOnly Cookie 认证
  • RTK Query 处理API请求

当前实现代码

SessionProvider组件

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

导航链接组件(NavLinks)

"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

登录组件(Login)

"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

核心问题

执行登录或登出操作后,React Context中的认证状态不会即时更新,必须刷新页面才能同步状态。尝试过useEffect、useState以及将Context替换为Redux Slice,均未解决问题。


解决方案

1. 给SessionProvider添加会话刷新能力

当前useVerifyQuery仅在组件挂载时执行一次,登录/登出后Cookie变化但查询不会自动重新运行。需要获取RTK Query的refetch方法并暴露给上下文:

"use client"
import { UserI } from '@/interfaces';
import { useVerifyQuery } from '@/lib/features/authApi';
import { createContext, PropsWithChildren, useCallback } from 'react';

interface ContextI {
    user: UserI | null;
    isAuthenticated: boolean;
    error: string | null;
    isLoading: boolean;
    refreshSession: () => void; // 添加刷新方法
}

export const SessionContext = createContext<ContextI>({
    user: null,
    isAuthenticated: false,
    error: null,
    isLoading: false,
    refreshSession: () => {},
});

export const SessionProvider = ({ children }: PropsWithChildren) => {
    const { data, isLoading, refetch } = useVerifyQuery(); // 获取refetch方法

    const user = data?.data;
    const error = data?.error;

    // 定义刷新会话的方法
    const refreshSession = useCallback(() => {
        refetch();
    }, [refetch]);

    if (error) console.log(error);

    return (
        <SessionContext.Provider 
            value={{ 
                user: user ?? null, 
                isAuthenticated: user ? true : false, 
                error: error ?? null, 
                isLoading,
                refreshSession // 传入上下文
            }}
        >
            {children}
        </SessionContext.Provider>
    );
};

2. 在登录/登出完成后触发会话刷新

修改Login组件,登录成功后刷新状态

const Login = () => {
    // ... 原有代码
    const { refreshSession } = useContext(SessionContext); // 引入刷新方法

    const onSubmit = async (data: AuthI) => {
        try {
            await login(data);
            await refreshSession(); // 登录后立即刷新会话状态
            navigation.push("/");
        } catch (error) {
            toast.error(unexpectedError.message, { toastId: unexpectedError.type });
            console.log(error);
        }
    };

    // ... 原有代码
}

修改Logout组件,登出后刷新状态

假设Logout组件逻辑如下,添加刷新调用:

"use client"
import { useLogoutMutation } from '@/lib/features/authApi';
import { useContext } from 'react';
import { SessionContext } from '@/app/SessionProvider';
import { useRouter } from 'next/navigation';

const Logout = ({ name }: { name?: string }) => {
    const [logout] = useLogoutMutation();
    const { refreshSession } = useContext(SessionContext);
    const router = useRouter();

    const handleLogout = async () => {
        try {
            await logout();
            await refreshSession(); // 登出后立即刷新会话状态
            router.push("/");
        } catch (error) {
            console.error(error);
        }
    };

    return (
        <button onClick={handleLogout}>Logout {name}</button>
    );
};

export default Logout;

3. 可选:优化RTK Query缓存配置

在authApi的查询定义中,添加自动重查规则,确保Cookie变化时状态同步:

// 在authApi的定义文件中
export const authApi = createApi({
    reducerPath: 'authApi',
    baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
    endpoints: (builder) => ({
        verify: builder.query({
            query: () => '/auth/verify',
            refetchOnMountOrArgChange: true, // 组件挂载时自动重查
            refetchOnReconnect: true, // 网络恢复时自动重查
        }),
        // ...其他端点
    }),
});

问题根源说明

  • 之前用useState但未和RTK Query的缓存状态同步,导致Context无法感知后端认证状态变化。
  • useEffect无法直接监听Cookie变化,只能通过主动触发查询刷新来同步状态。
  • 换成Redux Slice后未在登录/登出后重新拉取用户信息,Slice状态与后端认证状态脱节。

内容的提问来源于stack exchange,提问作者Scorpion King

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 21:37:07