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

React Hook组件未随useSelector更新状态重新渲染问题排查

问题:React + Redux应用中useEffect派发action后组件不重新渲染,Loading持续显示

场景说明

我开发了一个React + Redux应用,主组件<App>通过无依赖的useEffect派发action请求用户信息、元数据等数据,数据返回前显示“Loading”提示。但实际测试发现:action执行完成后,Redux状态已通过开发工具确认更新,但组件并未重新渲染,“Loading”一直显示。

若不使用useEffect,直接在组件内编写逻辑,功能可正常运行,但代码结构不够优雅。


相关代码

App组件核心代码

import { Outlet, Route, Routes } from 'react-router-dom';
import Login from './public/Login';
import './App.module.scss';
import Layout from "../../common/partials/Layout";
import Requests from "./private/Requests";
import Profile from "./private/Profile";
import AuthValidator from "../../common/components/AuthProvider";
import Register from "./public/Register";
import { useDispatch, useSelector } from "react-redux";
import { getUserData } from '../actions/auth';
import Home from "./Home";
import { State } from "../store";
import VerifyEmail from "./public/VerifyEmail";
import { onMobileLayoutSet } from '../../common/actions/layout';
import ProfileWizard from "./private/profile_wizard/ProfileWizardContainer";
import { getMetaData } from "../../common/actions/metadata";

const App = () => {
    const dispatch = useDispatch();
    const {
        auth: { isLogged },
        layout: { isMobile },
        metadata: { countries },
        profile: { profileExists }
    } = useSelector((state: State) => state);

    useEffect( () => {
        dispatch(getUserData());
        dispatch(getMetaData());

        const { innerWidth: width } = window;
        if (width < 1400 && !isMobile) {
            dispatch(onMobileLayoutSet());
        }
    }, []);

    if (isLogged == null || (isLogged && countries.length == 0) || profileExists == null) {
        return (
            <div>Loading</div>
        );
    }

    return (
        <Routes>
            <Route path={ "/" } element={ <Home/> }/>
            <Route
                element={ <AuthValidator loginAddress={ '/login' } defaultAddress={ '/requests' } isLogged={ isLogged }><Outlet/></AuthValidator> }>
                <Route element={ <AuthValidator.Block mustBeLogged={ false }><Outlet/></AuthValidator.Block> }>
                    <Route path={ "/login" } element={ <Login/> }/>
                    <Route path={ "/register" } element={ <Register/> }/>
                    <Route path={ "/verify-email" } element={ <VerifyEmail/> }/>
                </Route>
                <Route element={ <AuthValidator.Block mustBeLogged={ true }><Outlet/></AuthValidator.Block> }>
                    <Route element={ <Layout isMobile={ isMobile }><Outlet/></Layout> }>
                        <Route path={ "/requests" } element={ <Requests/> }/>
                        <Route path={ "/profile" } element={ <Profile/> }/>
                    </Route>
                    <Route path={ "/profile-wizard" } element={ <ProfileWizard/> }/>
                </Route>
            </Route>
        </Routes>
    );
};

export default App;

非useEffect的可行(但不优雅)逻辑

// useEffect( () => {
//     dispatch(getUserData());
//     dispatch(getMetaData());
//
//     const { innerWidth: width } = window;
//     if (width < 1400 && !isMobile) {
//         dispatch(onMobileLayoutSet());
//     }
// }, []);

if (isLogged == null && !userDataIsBeingRequested) {
    dispatch(getUserData());
}

if(countries.length === 0 && !metadataRequested) {
    dispatch(getMetaData());
}

const { innerWidth: width } = window;
if (width < 1400 && !isMobile) {
    dispatch(onMobileLayoutSet());
}

示例Action Creator(自定义API中间件)

export const login = ({ email }) => requestAction({
    actionType: 'LOGIN',
    url: '/api/worker/login',
    onRequest: () => {
        return { type: AuthActions.LoginAttempt, email }
    },
    onError: (responseData) => {
        if (responseData.data.error.code === 'UserNotFound') {
            return onLoginError(responseData.data.error.message);
        }
    },
    onSuccess: () => onLoginSuccess(),
    method: RequestMethod.Post,
    data: { email },
});

示例Auth Reducer

export enum Actions {
    LoginAttempt = "LoginAttempt",
    LoginError = "LoginError",
    LoginSuccess = "LoginSuccess",
    GetUserDataSuccess = "GetUserDataSuccess",
    GetUserDataFail = "GetUserDataFail",
    GetUserDataAttempt = "GetUserDataAttempt",
    RegisterAttempt = "RegisterAttempt",
    RegisterError = "RegisterError",
    RegisterSuccess = "RegisterSuccess",
    BackToSignUp = "BackToSignUp",
    EmailChanged = "EmailChanged",
    LogoutAttempt = "LogoutAttempt",
    LogoutSuccess = "LogoutSuccess",
    LogoutFail = "LogoutFail",
}

export type AuthState = {
    passwordSent: boolean,
    isLogged: boolean | null,
    isLoading: boolean,
    userId: string | null,
    firstName: string | null,
    lastName: string | null,
    number: number | null,
    email: string | null,
    userDataIsBeingRequested: boolean,
    registrationComplete: boolean
    loginComplete: boolean
    emailErrorMessage: string | null
}

const initialState: AuthState = {
    passwordSent: false,
    isLogged: null,
    isLoading: false,
    registrationComplete: false,
    userId: null,
    firstName: null,
    lastName: null,
    number: null,
    email: null,
    userDataIsBeingRequested: false,
    emailErrorMessage: null,
    loginComplete: false
};

const authReducer = (state = initialState, action) => {
    switch (action.type) {
        case Actions.GetUserDataSuccess: {
            return { ...state, ...action.userInfo, isLogged: action.isLogged, userDataIsBeingRequested: false };
        }
        case Actions.GetUserDataFail: {
            return { ...state, isLogged: false, userDataIsBeingRequested: false };
        }
        case Actions.GetUserDataAttempt: {
            return { ...state, userDataIsBeingRequested: true };
        }
        case Actions.RegisterAttempt: {
            return {...state, isLoading: true, email: action.email};
        }
        case Actions.RegisterSuccess: {
            return {...state, isLoading: false, registrationComplete: true};
        }
        case Actions.BackToSignUp: {
            return {...state, email: null};
        }
        case Actions.RegisterError: {
            return {...state, emailErrorMessage: action.emailErrorMessage, isLoading: false}
        }
        case Actions.EmailChanged: {
            return {...state, emailErrorMessage: null}
        }
        case Actions.LoginAttempt: {
            return {...state, isLoading: true}
        }
        case Actions.LoginError: {
            return {...state, emailErrorMessage: action.emailErrorMessage, isLoading: false}
        }
        case Actions.LoginSuccess: {
            return {...state, loginComplete: true, isLoading: false}
        }
        default:
            return state;
    }
};

export default authReducer;

问题分析与解决方案

核心排查点

  1. 加载条件的状态字段未被正确更新
    你的Loading判断条件包含profileExists == null,但从提供的代码来看,getUserData仅更新auth slice的字段,并未触发profile slice中profileExists的更新。如果没有其他action更新该字段,它会一直保持初始值null,导致Loading条件始终成立。

    解决:

    • 检查profileExists的更新逻辑,确保在getUserData完成后有对应action更新该字段;
    • 若该字段并非Loading必需项,可暂时从判断条件中移除,验证是否能正常退出Loading。
  2. Redux状态更新的引用问题
    Redux的useSelector默认用严格相等(===)比较前后状态,若reducer更新时未返回新的对象引用(未使用...state展开),会导致useSelector认为状态未变化,不触发组件重新渲染。需检查metadata和profile slice的reducer是否遵循相同规范。

  3. useSelector的选择逻辑优化
    建议精准选择所需字段,减少不必要的渲染触发:

    const isLogged = useSelector((state: State) => state.auth.isLogged);
    const countries = useSelector((state: State) => state.metadata.countries);
    const profileExists = useSelector((state: State) => state.profile.profileExists);
    const isMobile = useSelector((state: State) => state.layout.isMobile);
    
  4. useEffect依赖数组规范
    虽然useDispatch返回的dispatch是稳定的,但建议将其加入useEffect的依赖数组,符合React Hooks规范:

    useEffect( () => {
        // ...逻辑
    }, [dispatch]);
    

验证步骤

  1. 打开Redux DevTools,确认profileExists字段在数据请求完成后是否被更新为非null值;
  2. 暂时移除profileExists == null的判断,看组件是否能正常渲染Routes;
  3. 检查metadata reducer是否在getMetaData成功时,正确将countries设置为非空数组。

内容的提问来源于stack exchange,提问作者Andrey Inihov

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 05:42:26