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:
- Server renders:
<ion-app><div class="ion-page"><ion-header>...
- Plain web component tags, no classes.
- 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">...
- 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 timeThis 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 />);
}