Fix SSR hydration mismatch for auto-generated editor id (supersedes #495) - #514
Merged
Conversation
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 Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
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.
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 expliciteditorId, 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 callsunlayer.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 nouseIdand 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 forunlayer.createEditor. Passing an expliciteditorIdis unaffected — it still wins.Verification (post-rebase, local)
npm test— 12 passing (10 existing + 2 new intest/ssr.test.tsx, which renders withrenderToStringthen hydrates withhydrateRootand asserts a stable id + no hydration warning).npm run lint,npm run typecheck,npm run build— clean.Conflict resolution
Only
CHANGELOG.mdconflicted. Resolved by keeping the released2.1.1/2.1.0/2.0.0sections and placing the SSR fix under a new## Unreleasedsection at the top.Co-authored-by: jawad Akram jawad.akram347@gmail.com