【问题标题】:Can't perform a React state update on an unmounted component when using useEffect hook使用 useEffect 挂钩时无法对未安装的组件执行 React 状态更新
【发布时间】:2021-08-25 12:09:26
【问题描述】:

我在使用useEffect hook 时似乎遇到了以下错误。

警告:无法对未安装的组件执行 React 状态更新。这是一个空操作,但它表明您的应用程序中存在内存泄漏。要解决此问题,请在 useEffect 清理函数中取消所有订阅和异步任务。

我相信这与我调用的异步函数有关,以设置用户是否经过身份验证。

ProtectedRoute.tsx

export function ProtectedRoute({ ...routeProps }: ProtectedRouteProps): ReactElement | null {
    const context = useContext(AppContext);
    const [isAuthenticated, setIsAuthenticated] = useState(false);

    useEffect(() => {
        isUserAuthenticated(context.token).then(setIsAuthenticated).catch(setIsAuthenticated);
    });

    if (isAuthenticated) {
        return <Route {...routeProps} />;
    } else {
        return <Redirect to={{ pathname: "login" }} />;
    }
}

const isUserAuthenticated = async (token: any): Promise<boolean> => {
    try {
        const response = await VerifyAuthentication(token);
        console.log("VerifyAuthentication", response);
        if (response.status !== 200) {
            return false;
        }

        return true;
    } catch (err) {
        return false;
    }
};

App.tsx

class App extends Component<Props, State> {
    renderRouter = () => {
        return (
            <Router>
                <Switch>
                    <ProtectedRoute exact path="/" component={Dashboard} />
                    <Route exact path="/login" component={Login} />
                </Switch>
            </Router>
        );
    };

    render(): ReactElement {
        return (
            <div className="App">
                <AppProvider>
                    <Theme>
                        <Sidebar>{this.renderRouter()}</Sidebar>
                    </Theme>
                </AppProvider>
            </div>
        );
    }
}

【问题讨论】:

    标签: reactjs


    【解决方案1】:

    大概这会将用户重定向到没有此组件的路由:

    return <Redirect to={{ pathname: "login" }} />;
    

    这意味着组件已卸载,或者通常从活动使用/内存中卸载。而这种情况总是会发生,因为这种情况永远不会是true:

    if (isAuthenticated) {
    

    因为当组件第一次渲染时,该值被显式设置为false:

    const [isAuthenticated, setIsAuthenticated] = useState(false);
    

    所以基本上发生的事情是:

    1. 您触发了一个异步操作来检查用户是否通过了身份验证。
    2. 在等待响应之前,您决定用户未通过身份验证并将其重定向。
    3. 组件已卸载,因为用户已离开此页面。
    4. 接收到异步响应并尝试更新不再加载/安装的组件的状态。

    尚不完全清楚该组件是如何适应您的整体结构的,但您需要更改该结构。检查身份验证需要同步,或者您需要在重定向之前等待异步操作完成。后者的一个例子可能很简单:

    export function ProtectedRoute({ ...routeProps }: ProtectedRouteProps): ReactElement | null {
      const context = useContext(AppContext);
      const [isAuthenticated, setIsAuthenticated] = useState(false);
      const [isLoading, setIsLoading] = useState(true);
    
      useEffect(() => {
        isUserAuthenticated(context.token)
          .then(x => {
            setIsAuthenticated(x);
            setIsLoading(false);
          })
          .catch(e => {
            setIsAuthenticated(false);
            setIsLoading(false);
            console.log(e);
          });
      });
    
      if (isLoading) {
        return <div>Loading...</div>;
      } else if (isAuthenticated) {
        return <Route {...routeProps} />;
      } else {
        return <Redirect to={{ pathname: "login" }} />;
      }
    }
    

    在这种情况下,isLoading 的单独状态值用于跟踪异步操作是否仍在进行,因此组件“等待”直到加载数据,然后再决定是否重定向用户。

    但总的来说,我不明白为什么身份验证检查不能同步。更高级别的东西,例如将整个应用程序结构包装在&lt;App/&gt; 中的提供程序组件,可以具有与上述相同的逻辑,本质上是执行异步操作并将结果保持在状态中。然后可以通过 useContext 或 Redux 提供该状态,甚至可以将其作为 props 传递给所有子组件。

    您不需要在子组件中一遍又一遍地重新检查身份验证。这是应用程序级别的问题。

    【讨论】:

    • 非常清楚的解释谢谢,这解决了问题,所以我将其标记为解决方案。
    • 但是我在 isUserAuthenticated 函数中添加的控制台日志在页面加载时会触发 3 次,这意味着每当我刷新页面时它都会发出 3 个请求。
    • 这让我觉得这可能不是进行此身份验证检查的最佳位置,正如您建议的那样,我应该在 appContext 中进行身份验证检查,并在状态和使用中保存一个值 isAuthenticated这个状态值来自上下文
    • @Kay:页面上有多少这个组件的实例?它只是包装了一个&lt;Route/&gt;,所以如果它在你的整体结构中包装了不止一个,那么它就不必要地重复这个操作。身份验证检查应位于或非常接近组件层次结构的最高级别,而不是围绕单个组件。
    • @Kay:我会将这个功能放在整体结构中的&lt;Router/&gt; 之上。主要是因为您对路由库如何挂载/卸载事物没有太多控制权。它应该很可能靠近&lt;Theme/&gt; 或其他一些全局封装组件。将身份验证信息放在上下文或 Redux 或类似的东西中。然后&lt;ProtectedRoute/&gt; 组件可以只使用该上下文,而不是在内部重复身份验证检查。
    【解决方案2】:

    调用setIsAuthenticated时可以使用变量来检查组件是挂载还是卸载

    useEffect(() => {
      let isMouted = true;
      isUserAuthenticated(context.token)
        .then((val) => isMouted && setIsAuthenticated(val))
        .catch(setIsAuthenticated);
      return () => {
        isMouted = false;
      };
    });
    

    【讨论】:

    • 这会抑制错误,但不一定能解决问题。该组件将总是在异步操作完成之前卸载。
    猜你喜欢
    • 2020-08-02
    • 2020-07-19
    • 1970-01-01
    • 2021-05-15
    • 2022-01-18
    • 2019-07-24
    • 2021-12-03
    • 2020-12-20
    • 1970-01-01
    相关资源
    最近更新 更多