Skip to content

Fix SSR hydration mismatch for auto-generated editor id (supersedes #495) - #514

Merged
adeelraza merged 3 commits into
masterfrom
claude/pr495-ssr-hydration
Aug 11, 2026
Merged

Fix SSR hydration mismatch for auto-generated editor id (supersedes #495)#514
adeelraza merged 3 commits into
masterfrom
claude/pr495-ssr-hydration

Conversation

@adeelraza

Copy link
Copy Markdown
Contributor

Rebased and conflict-resolved version of #495 by @jawadakram20 (original commit authorship preserved). Supersedes and closes #495, which conflicted with the 2.1.0/2.1.1 CHANGELOG entries added since it was opened.

What it fixes

When <EmailEditor /> is used without an explicit editorId, the id was generated from a module-level counter (editor-${++counter}). Under SSR this diverges between server and client (a warm server keeps counting; every fresh browser starts at 0), producing a hydration mismatch. Because the editor then calls unlayer.createEditor({ id }) against an element whose id no longer matches, it mounts on a missing container and renders blank with no error — notably under the Next.js App Router, which this package targets via the 'use client' banner added in 2.0.0.

The fix

Use React 18+'s useId, whose value is identical on the server render and during client hydration. React 16.8/17 (still in the peer range) have no useId and keep the legacy counter fallback. The implementation is selected once at module load, so the same hook runs on every render (rules of hooks respected). useId's output is stripped of : so the id stays a valid CSS selector for unlayer.createEditor. Passing an explicit editorId is unaffected — it still wins.

Verification (post-rebase, local)

  • npm test12 passing (10 existing + 2 new in test/ssr.test.tsx, which renders with renderToString then hydrates with hydrateRoot and asserts a stable id + no hydration warning).
  • npm run lint, npm run typecheck, npm run build — clean.

Conflict resolution

Only CHANGELOG.md conflicted. Resolved by keeping the released 2.1.1/2.1.0/2.0.0 sections and placing the SSR fix under a new ## Unreleased section at the top.

Co-authored-by: jawad Akram jawad.akram347@gmail.com

The auto-generated editor id came from a module-level counter
(`editor-${++win.__unlayer_lastEditorId}`). On a warm SSR server the
counter keeps climbing across requests while every fresh client starts at
0, so the server and client render different ids. React does not patch the
attribute mismatch on hydration, and the mount effect then calls
`unlayer.createEditor` against the stale server id — a div that no longer
matches — leaving a silently blank editor under SSR (e.g. the Next.js App
Router, which this package already targets via the `'use client'` banner).

Use React 18+'s `useId`, whose value is identical on the server render and
during client hydration. React 16.8/17 (still in the peer range) have no
useId and keep the legacy counter fallback. Passing an explicit `editorId`
is unaffected.

Adds test/ssr.test.tsx, which reproduces the hydration mismatch (fails on
the old counter, passes with useId).
@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 96.25%. Comparing base (b418131) to head (f17e9e2).
⚠️ Report is 4 commits behind head on master.

Files with missing lines Patch % Lines
src/EmailEditor.tsx 75.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #514      +/-   ##
==========================================
+ Coverage   94.87%   96.25%   +1.37%     
==========================================
  Files           2        2              
  Lines          78       80       +2     
  Branches       20       20              
==========================================
+ Hits           74       77       +3     
+ Misses          1        0       -1     
  Partials        3        3              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Patch bump for the SSR/hydration editor-id fix (supersedes #495).

Version-only change on top of the fix commit; publish done separately.
Fire the registered editor:ready callback in the existing test and assert
onReady receives the editor instance. Closes a pre-existing line-coverage
gap (EmailEditor.tsx onReady branch) that Codecov's patch check surfaced on
the shifted diff for #514.
@adeelraza
adeelraza merged commit 0155e26 into master Aug 11, 2026
13 checks passed
pull Bot pushed a commit to stungkit/react-email-editor that referenced this pull request Aug 11, 2026
The useId/counter selection ternary added in unlayer#514 always leaves one arm
uncovered in any single coverage run: the run uses React 18/19 (so useId is
picked) and the React 16/17 counter fallback is exercised only by the
test:legacy smoke suite, which runs without coverage. That partial branch on
a new line tripped codecov/patch on master.

The fallback function was already v8-ignored; extend the ignore over the
selection ternary too. Comment-only change, no logic touched. EmailEditor.tsx
branch coverage 94.28% -> 96.96% (the remaining uncovered branch is the
pre-existing typeof-window SSR guard, unrelated to any patch).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants