fix(dom): use real microtask + force layout for enter connection wait - #73
Merged
Merged
Conversation
Follow-up to the previous release's connection wait. That version used Effect.yieldNow() to defer the enter fiber past the outer synchronous render flow, but yieldNow only reschedules inside Effect's own queue — if no other work is queued, the fiber can re-run immediately without the browser flushing microtasks or committing DOM. In practice this meant real client re-mounts still saw element.isConnected === false at the 3-attempt bound, the animation ran against a disconnected element, and the transition never fired. Switch to queueMicrotask via Effect.async — a real browser microtask guarantees the fiber won't resume until the current task's synchronous work and other queued microtasks have run. That's when the outer flow finishes inserting the wrapper's ancestor chain, so element becomes connected. Bounded at 32 attempts; each is submicrosecond in modern engines, so a full spin is well under 1ms even for callers whose element never gets inserted. Also force a style/layout computation via `element.offsetHeight` after connection. Some engines defer styling for freshly-inserted nodes until needed, and without this forceReflow in runEnterAnimation could sample an unstyled snapshot as the transition's "before" reference, leaving nothing for the browser to interpolate from. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Deploying effex with
|
| Latest commit: |
8e9426f
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://f6ed929a.effex.pages.dev |
| Branch Preview URL: | https://fix-enter-anim-connection-re.effex.pages.dev |
Deploying effex-api with
|
| Latest commit: |
8e9426f
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://81f07480.effex-api.pages.dev |
| Branch Preview URL: | https://fix-enter-anim-connection-re.effex-api.pages.dev |
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.
Summary
Follow-up to #71. That fix used
Effect.yieldNow()to wait for connection — butyieldNowonly reschedules inside Effect's own queue. If no other work is queued, the fiber runs again immediately, without the browser flushing microtasks or committing DOM. In practice the connection check still sawisConnected === falseon real re-mounts, we hit the 3-attempt bound, and proceeded against a disconnected element. User confirmed post-merge that the exact same failure was still happening.Fix
Two changes:
1. Real microtask via
queueMicrotask. ReplaceEffect.yieldNow()withEffect.async<void>(resume => queueMicrotask(() => resume(Effect.void))).queueMicrotaskis a real browser primitive that guarantees the callback runs after the current task's synchronous work and other pending microtasks — i.e. after the outer render flow finishes appending the wrapper's ancestor chain to the document. Loop bounded at 32 attempts; each microtask is submicrosecond, so a full spin is well under 1ms even for callers whose element never gets inserted (e.g. tests).2. Force style computation via
offsetHeight. After the element is connected, readelement.offsetHeightto force the browser to compute layout + styles. Some engines defer style computation for freshly-inserted nodes until it's needed; without this, theforceReflowinsiderunEnterAnimationcould sample an unstyled snapshot as the transition's "before" reference, leaving nothing for the browser to interpolate from.Diagnostic that led here
User pasted their
onBeforeEntersnapshot on nav-back:classNamecorrect (enterFrom applied), but every computed-style field is empty — the tell for a disconnected node. Confirmed #71'sEffect.yieldNowwasn't actually flushing microtasks.Tests
Existing regression test from #71 (
onBeforeEnter fires against an attached element (nested remount)) still passes with the new implementation. 788 tests pass across the workspace.tsc --noEmitclean.Changeset:
@effex/dompatch.🤖 Generated with Claude Code