SSR and hydration mismatch with ionic components

I asked Claude what is causing the hydration mismatch errors in the browser console in my barebones Ionic+SSR starter project:

Timeline of the problem:

  1. Server renders:
<ion-app><div class="ion-page"><ion-header>...
  1. Plain web component tags, no classes.
  2. Browser receives HTML and loads JavaScript:
  • Ionic’s JavaScript initializes and “upgrades” the web components
  • Adds classes like “md”, “hydrated”, “ion-page”, etc.
  • This happens BEFORE React hydration starts
<ion-app class="md ion-page hydrated"><div class="ion-page"><ion-header role="banner" class="md header-md header-collapse-none hydrated">...
  1. React tries to hydrate:
  • Expects plain <ion-app> (matching SSR output)
  • But finds <ion-app class="md ion-page hydrated"> (already upgraded by Ionic)
  • Mismatch! React throws a hydration error

Root Cause

Ionic’s web components are being upgraded by Ionic’s JavaScript before React hydration runs. This creates a mismatch between:

  • What React expects (plain tags from SSR)
  • What’s in the DOM (upgraded web components with classes)

The culprit is in App.tsx:

setupIonicReact();  // Called at module load time

This runs when the module loads, which is before React hydration. Ionic starts upgrading web components immediately.

Is this really what’s happening?

How can I work around it?

App.tsx:

import {
  IonApp,
  setupIonicReact,
  IonPage,
  IonHeader,
  IonToolbar,
  IonTitle,
  IonContent,
} from '@ionic/react';

import '@ionic/react/css/core.css';
import '@ionic/react/css/normalize.css';
import '@ionic/react/css/structure.css';
import '@ionic/react/css/typography.css';

setupIonicReact();

function App() {
  return (
    <IonApp>
      <IonPage>
        <IonHeader>
          <IonToolbar>
            <IonTitle>Ionic App</IonTitle>
          </IonToolbar>
        </IonHeader>
        <IonContent>
          MRE
        </IonContent>
      </IonPage>
    </IonApp>
  );
}

export default App;

entry-client.tsx:

import { hydrateRoot } from 'react-dom/client'
import App from './App.tsx'

hydrateRoot(
  document.getElementById('root')!,
  <App />
)

entry-server.tsx:

import { renderToString } from 'react-dom/server';
import App from './App';

export function render() {
  return renderToString(<App />);
}

Few days ago I enlighten you that ionic currently does not support SSR. No matter what Ai you ask, it will NOT work.

1 Like

Well, SSR does work, it’s just react’s out-of-the-box hydrateRoot that doesn’t seem to be compatible with ionic