ionic-framework: bug: react - maximum depth exceeded from PrivateRoute

Bug Report

Ionic version: [ ] 4.x [x] 5.x

Current behavior: Redirecting from a PrivateRoute causes “Maximum update depth exceeded”.

Expected behavior: Redirect to, for example, a “/login” page.

I have been trying to use the PrivateRoute pattern described here without success.

Steps to reproduce:

ionic start ionic-react-redirect-bug conference --type=react cd ionic-react-redirect-bug

Add the following to src/App.tsx:

interface PrivateRouteProps extends RouteProps {
  component: any;
}

const PrivateRoute = ({component: Component, ...rest}: PrivateRouteProps) => {
  const isLoggedIn = false;

  return (
    <Route
      {...rest}
      render={(routeProps) =>
        isLoggedIn ? (
          <Component {...routeProps} />
        ) : (
          <Redirect to={{pathname: "/login", state: {from: routeProps.location}}}/>
        )
      }
    />
  );
}

In src/App.tsx replace:

<Route path="/support" component={Support} />

with:

<PrivateRoute path="/support" component={Support} />

Comment out useEffect in src/components/Map.tsx if it complains about anything (unrelated to this issue).

ionic serve

Click the Support menu item.

Other information:

In the browser console:

Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
    React 4
    unlisten react-router.js:69
    listener history.js:162
    notifyListeners history.js:180
    notifyListeners history.js:179
    setState history.js:300
    replace history.js:414
    confirmTransitionTo history.js:152
    replace history.js:397
    onUpdate react-router.js:309
    componentDidUpdate react-router.js:189
    React 6
    unstable_runWithPriority scheduler.development.js:659
    React 5
    unstable_runWithPriority scheduler.development.js:659
    React 6

Ionic info:

Ionic:

   Ionic CLI       : 6.10.1 (/usr/local/lib/node_modules/@ionic/cli)
   Ionic Framework : @ionic/react 5.2.3

Capacitor:

   Capacitor CLI   : 1.3.0
   @capacitor/core : 1.3.0

Utility:

   cordova-res : not installed
   native-run  : 1.0.0

System:

   NodeJS : v14.4.0 (/usr/local/Cellar/node/14.4.0/bin/node)
   npm    : 6.14.6
   OS     : macOS Catalina

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 2
  • Comments: 18 (9 by maintainers)

Commits related to this issue

Most upvoted comments

Awesome, I am going to close this issue out as completed, as the main issue reported is resolved.

If any run into any other issues, feel free to open a new issue and the team would be happy to take a look.

Hello @m3thom looking into this issue again.

I believe the routing structure may be a driving factor to the issue here.

The /login page does not fit within mobile tabs navigation rules. The route is a root route, but is being rendered inside of the tabs router outlet, having the tab bar being displayed when the route is active. The tab bar should only be displayed for tab pages, or sub-pages of tabs (that are within the stack).

There are two valid solutions I see with this use case:

Login is a root route outside of the tabs outlet

Treating the /login page as a page outside of the tabs outlet will result in the page being full height (not displaying the tab bar). This pattern is most often use when the entire tabs experience is protected by authentication.

e.g.:

const App: React.FC = () => (
  <ProvideAuth>
    <IonApp>
      <IonReactRouter>
        <IonRouterOutlet>
          <Route exact path="/login">
            <Login />
          </Route>
          <IonTabs>
            <IonRouterOutlet>
              <Route exact path="/tab1">
                <Tab1 />
              </Route>
              <Route exact path="/tab2">
                <Tab2 />
              </Route>

              <PrivateRoute path="/tab3">
                <Tab3 />
              </PrivateRoute>

              <Route exact path="/">
                <Redirect to="/tab1" />
              </Route>
            </IonRouterOutlet>
            <IonTabBar slot="bottom">
              <IonTabButton tab="tab1" href="/tab1">
                <IonIcon icon={triangle} />
                <IonLabel>Tab 1</IonLabel>
              </IonTabButton>
              <IonTabButton tab="tab2" href="/tab2">
                <IonIcon icon={ellipse} />
                <IonLabel>Tab 2</IonLabel>
              </IonTabButton>
              <IonTabButton tab="tab3" href="/tab3">
                <IonIcon icon={square} />
                <IonLabel>Tab 3</IonLabel>
              </IonTabButton>
            </IonTabBar>
          </IonTabs>
        </IonRouterOutlet>
      </IonReactRouter>
    </IonApp>
  </ProvideAuth>
);

<video src="https://user-images.githubusercontent.com/13732623/201206944-952dfece-c028-4397-9cdb-fa9455cf33e2.mp4"></video>

I did have to tweak some of the logic around the location state, repro: https://github.com/sean-perkins/ionic-gh-21717

Use a modal in place of a protected route

If the true desire is to allow unauthenticated users to access tab 1 and tab 2, but have to sign into access tab 3, a modal experience that takes over the fullscreen when tab 3 is activated would be desired.

I experimented with trying to render the private route inside of the tab3 routing structure, but this leads to other problems, that activating the tab bar button for tab3 attempts to route to /tab3, but is redirected to /tab3/login when unauthenticated. This results in a push transition which looks bad and is probably an anti-pattern for navigation.

Can you try either of these suggestions and let me know if that resolves your issue or if you observe anything else?

I tried this in App.tsx which stopped the error:

<Redirect to={{pathname: "/login", state: {from: {pathname: routeProps.location.pathname}}}}/>

And this in Login.tsx:

interface MyLocationState {
  from: {pathname: string};
}

...

  const location = useLocation<MyLocationState>();
  const { from } = location.state || { from: { pathname: "/" } };
  console.log("from:", from);

  const login = async (e: React.FormEvent) => {
    e.preventDefault();
    setFormSubmitted(true);
    await setIsLoggedIn(true);
    await setUsernameAction(username);
    history.replace(from);
    // history.push('/tabs/schedule', {direction: 'none'});
  };

But as seen from the log, the value is overwritten so the redirect won’t work:

from:  Object { pathname: "/" } Login.tsx:31
from:  Object { pathname: "/support" } Login.tsx:31
from:  Object { pathname: "/login" } Login.tsx:31

Hi @jeffcjohnson,

I noticed passing in the routeProps.location to the state is what is seeming to cause an infinite render somewhere inside react router. This is happening because the value of location changes each render. Are you able to pull off just the pathname from location, instead of passing the whole object?:

<Redirect to={{pathname: "/login", state: {from: routeProps.location.pathname}}}/>