'Page is not rendering after using `history.push` in Ionic/React with `IonTabs` as nested routes

I have an Ionic/React v5.0.7 app with IonReactRouter to manage routing:

// App.tsx

return (
  <IonApp>
    <IonReactRouter history={history}>
      <IonRouterOutlet>
        <Route exact path={ROUTES.HOME} component={Home} />
        <Route exact path={ROUTES.LOGIN} component={Login} />
        <Route path={ROUTES.CONTRACTOR} component={ContractorDashboard} />
      </IonRouterOutlet>
    </IonReactRouter>
  </IonApp>
)

In one of my routes (path={ROUTES.CONTRACTOR}) I'm trying to implement IonTabs which needs to have its own IonRouterOutlet, so I have the tabs as nested routes like so:

// ContractorDashboard.tsx

<IonContent>
  <IonTabs>
    <IonTabBar slot="top">
      <IonTabButton href="/contractor/overview" tab="contractor-overview">
        <IonLabel>{t('ContractorDashboard.contractorOverview')}</IonLabel>
      </IonTabButton>
      <IonTabButton href="/contractor/contractor-configs" tab="contractor-configs">
        <IonLabel>{t('ContractorDashboard.contractorConfig')}</IonLabel>
      </IonTabButton>
      <IonTabButton href="/contractor/workers-management" tab="workers-management">
        <IonLabel>{t('ContractorDashboard.workersManagement')}</IonLabel>
      </IonTabButton>
      <IonTabButton href="/contractor/email-responses" tab="email-responses">
        <IonLabel>Some label</IonLabel>
      </IonTabButton>
    </IonTabBar>

    <IonRouterOutlet>
      <Route path="/contractor/contractor-configs" render={() => <ContractorConfigs userId={authUser.uid} />} />
      <Route path="/contractor/workers-management" component={WorkersManagement} />
      <Route path="/contractor/email-responses" render={() => <EmailResponses userId={authUser.uid} />} />
      <Route path="/contractor/contractor-overview" render={() => <IonTitle>Title</IonTitle>} />
      <Route exact path="/contractor" render={() => <Redirect to="/contractor/overview" />} />
    </IonRouterOutlet>
          
  </IonTabs>
</IonContent>

Everything works fine until I try to create dynamic navigation to /contractor route, which is the root of the tabs' router, using history.push('/contractor') or <Redirect to="/contractor" />.

In my login component I want to redirect automatically to the ContractorDashboard if a user is authenticated. I.e. after a successful login the user should be redirected to the dashboard. I implemented the following:

// Login.tsx

useEffect(() => {
  if (authUser) {
    history.push('/contractor')
  }
}, [authUser, history])

What happens is the URL is changed in the address bar but the login page is still visible instead of the ContractorDashboard. I also tried this:

// Login.tsx

if (authUser) {
  return <Redirect to="/contractor"/>
}

return (
// ...
)

The result is the same. I can see the contractor page in the dev tools but it's not rendered.
A few notes though:

  • The result is a bit different when I try different things, like pushing to /contractor/overview insted of just /contractor. Then the contractor dashboard is visible for a sec and then it's the login page again.
  • Sometimes I do get the dashboard, but the tabs are not visible. Its in the DOM and the cursor events are there but still not visible.
  • Everything was working before using IonTabs in the dashboard.
  • What's weird is that when I try to navigate to the login page via the address bar with an authenticated user I'm redirected to the dashboard like I should and everything is working.

I did try replacing IonReactRouter with Router from react-router like suggested in this answer. Then redirecting from the login page works but the tabs trigger a full page refresh on every tab change, which is just awful UX.

Here is my ionic info output:

Ionic:

   Ionic CLI : 6.11.0

Utility:

   cordova-res : not installed
   native-run  : not installed

System:

   NodeJS : v12.18.3
   npm    : 6.14.6
   OS     : Windows 10

and here's my package.json:

{
  "name": "application",
  "version": "0.0.1",
  "private": true,
  "dependencies": {
    "@capacitor/android": "^2.4.6",
    "@capacitor/core": "2.4.6",
    "@ionic/react": "^5.0.7",
    "@ionic/react-router": "^5.0.7",
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.4.0",
    "@testing-library/user-event": "^8.0.3",
    "@types/jest": "^24.0.25",
    "@types/node": "^12.12.24",
    "@types/react": "^16.9.17",
    "@types/react-dom": "^16.9.4",
    "@types/react-router": "^5.1.4",
    "@types/react-router-dom": "^5.1.3",
    "firebase": "^8.2.3",
    "i18next": "^19.8.4",
    "i18next-http-backend": "^1.0.22",
    "ionicons": "^5.0.0",
    "react": "^16.13.0",
    "react-dom": "^16.13.0",
    "react-i18next": "^11.8.5",
    "react-redux": "^7.2.2",
    "react-router": "^5.1.2",
    "react-router-dom": "^5.1.2",
    "react-scripts": "^4.0.1",
    "redux": "^4.0.5",
    "redux-localstorage-simple": "^2.3.1",
    "redux-thunk": "^2.3.0",
    "typescript": "3.8.3"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "appium": "appium --chromedriver-executable C:\\Users\\Ori\\code\\chromedrivers\\chromedriver-83-0-4103.exe",
    "e2e:android": "protractor e2e/android-e2e.conf.js",
    "e2e:web": "protractor e2e/web-e2e.conf.js"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "@capacitor/cli": "2.4.6",
    "@types/jasmine": "^3.6.3",
    "@types/jasminewd2": "^2.0.8",
    "@types/react-redux": "^7.1.15",
    "@types/redux-logger": "^3.0.8",
    "appium": "^1.20.2",
    "jasmine": "^3.6.4",
    "jasmine-spec-reporter": "^6.0.0",
    "protractor": "^7.0.0",
    "redux-logger": "^3.0.6",
    "ts-node": "^9.1.1"
  },
  "description": "An Ionic project"
}

So is there a way to properly implement this and how?
Please let me know if any info or code is missing here.
Any help will be greatly appreciated!

Update
So for anyone who is interested, I gave up looking for a solution. Instead I implemented my own basic tabs, without routes (never understood why tabs require routing anyway...).
Disregarding styles it's very simple:


const [tab, setTab] = useState<number>(0);
const mapTabs = [
  'contractorOverview',
  'contractorConfigs',
  'workersManagement',
  'emailResponses'
]

const handleTabChange = () => {
  switch (tab) {
    case 0: return <h3>Overview</h3>;
    case 1: return authUser && <ContractorConfigs userId={authUser.uid} />;
    case 2: return <WorkersManagement />;
    case 3: return authUser && <EmailResponses userId={authUser.uid} />;
    default: return <h3>Overview</h3>;
  }
}

return (
  // ....
  <IonContent>
    <IonToolbar color="secondary">
      <IonButtons slot="start">
        {mapTabs.map((_tab, tabIdx) => (
          <IonButton
            key={tabIdx}
            id={`${_tab}`}
            onClick={() => setTab(tabIdx)}
            disabled={mapTabs[tab] === _tab}
          >
            {t(`ContractorDashboard.${_tab}`)}
          </IonButton>
        ))}
      </IonButtons>
    </IonToolbar>

    {handleTabChange()}

  </IonContent>
  // ....
)

And that's all you need to have a working tab view (again, without some styling it won't look so nice).
If someone has a solution to the tabs issue (actually I'm not 100% sure what caused the problem... I wasn't able to reproduce in stackblitz...) I would love to hear it.
If not I will post this as an answer...



Solution 1:[1]

Have you tried:

history.push({ pathname: '/contractor' })

or

history.push({ pathname: '/contractor/overview' }); 

Solution 2:[2]

did you try...

history.push('/contractor/overview')

can you post a small project on stackblitz or codesandbox?

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Luís Silva
Solution 2 Aaron Saunders