简体   繁体   中英

Can't perform a React state update on an unmounted component when using useEffect hook

i seem to be getting the following error when using useEffect hook.

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

I believe it has something to do with the async function i am calling to set if the user is authenticated or not.

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

Presumably this redirects the user to a route which doesn't have this component:

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

Which means the component is unmounted, or generally unloaded from active use/memory. And this always happens, because this condition will never be true :

if (isAuthenticated) {

Because when the component first renders that value is explicitly set to false :

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

So basically what's happening is:

  1. You fire off an asynchronous operation to check if the user is authenticated.
  2. Before waiting for the response, you decide that the user is not authenticated and redirect them.
  3. The component is unloaded because the user has left this page.
  4. The asynchronous response is received and tries to update state for a component that is no longer loaded/mounted.

It's not entirely clear how this component is intended to fit into your overall structure, but you're going to need to change that structure. Either checking for authentication would need to be synchronous or you'd need to wait for the asynchronous operation to complete before redirecting. An example of the latter could be as simple as:

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

In that scenario a separate state value of isLoading is used to track whether the asynchronous operation is still taking place, so the component "waits" until the data is loaded before deciding to redirect the user or not.

But overall I don't see why the authentication check can't be synchronous. Something higher-level, such as a provider component that wraps the entire application structure within <App/> , can have this same logic above, essentially performing the async operation and keeping the result in state. Then that state can be provided via useContext or Redux or even just passing as props to all child components.

You shouldn't need to re-check for authentication over and over in child components. That's an application-level concern.

You can use a variable to check component is mount or unmount when call setIsAuthenticated

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM