Skip to content

fix(router): Outlet now actually applies its animation configuration - #84

Merged
jonlaing merged 2 commits into
mainfrom
fix/outlet-animation-config
Aug 7, 2026
Merged

fix(router): Outlet now actually applies its animation configuration#84
jonlaing merged 2 commits into
mainfrom
fix/outlet-animation-config

Conversation

@jonlaing

@jonlaing jonlaing commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Summary

OutletConfig.animate was defined in the type but never read in the implementation — the underlying reconcile call passed only getTargetKeys and renderSlot, so nothing wired the animation config through to the control ctx. Consumers configuring animate saw abrupt route transitions regardless of what they set.

Outlet now provides AnimationConfigCtx (the same tag when/match/each use) via Effect.provideService, matching the pattern those combinators follow.

Also adds an intro?: boolean field to OutletConfig, so the initially matched route can re-animate on hydration in cases like a decorative opening scene. Same shape as when/match/each/animated.

Scope-safety note

Effect.provideService uses provideContext internally rather than provideSomeLayer's scopedWith, so it does not create a fresh scope tied to the effect's lifetime the way Effect.provide(effect, Layer) does. That's the mechanism behind #78's finalizer race — the subscription fibers reconcile forks inherit the merged context and keep it for their lifetime, even after Outlet's outer Effect.gen returns. Same pattern when/match/each have been using without issue.

Also

  • AnimationConfigCtx and ClientControlCtx are now re-exported from @effex/dom's package root (they were exported from @effex/dom/Control/index.ts but not lifted to the package index).

Regression test

packages/router/src/Outlet.test.ts — two tests:

  1. Configuring animate makes AnimationConfigCtx observable from inside a route's render function.
  2. Omitting animate/intro keeps the config absent (Effect.serviceOption returns Option.none()), so downstream code treats the slot as non-animated.

Verified the first test fails against the pre-fix Outlet.

Test plan

  • Full packages/router + packages/dom suite: 467 passed, 2 skipped
  • Regression test confirmed to fail against pre-fix Outlet, pass after

🤖 Generated with Claude Code

jonlaing and others added 2 commits August 6, 2026 16:51
OutletConfig.animate was defined in the type but never read in the
implementation — the underlying reconcile call passed only
getTargetKeys and renderSlot, so nothing wired the animation config
through to the control ctx. Consumers configuring `animate` saw abrupt
route transitions regardless of what they set.

Outlet now provides AnimationConfigCtx (the same tag when/match/each
use) via Effect.provideService, matching the pattern those combinators
follow. provideService uses provideContext internally rather than
provideSomeLayer's scopedWith — no scope is created and no finalizer
race is introduced (see #78 for the finalizer-race pattern we're
deliberately avoiding).

Also adds an `intro?: boolean` field to OutletConfig, so the initially
matched route can re-animate on hydration in cases like a decorative
opening scene. Same shape as when/match/each/animated.

AnimationConfigCtx and ClientControlCtx are now re-exported from
@effex/dom's package root (they were exported from
@effex/dom/Control/index.ts but not lifted).

Regression test in packages/router/src/Outlet.test.ts verifies that
setting `animate` makes AnimationConfigCtx observable from inside a
route's render function, and that omitting it keeps the config absent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The intro-flag hydration tests waited 20ms after `hydrate()` for enter
animations to invoke `onBeforeEnter`. That was too tight now that
`forkSlotEnter` waits one requestAnimationFrame on the hydration path
(introduced in #83 to fix a first-load CSS-application race) — in
jsdom rAF is roughly a setTimeout(16), plus queue drain and the
subsequent enter lifecycle work. Local runs stayed just under the
margin; CI runners occasionally didn't.

Bumped the three affected tests to 100ms. Also bumped the "does not
re-animate SSR items by default" wait — the assertion is `not
toHaveBeenCalled`, so a longer wait protects against late-firing
regressions rather than hiding them.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 7, 2026

Copy link
Copy Markdown

Deploying effex-api with  Cloudflare Pages  Cloudflare Pages

Latest commit: 275826e
Status: ✅  Deploy successful!
Preview URL: https://ca16d8c7.effex-api.pages.dev
Branch Preview URL: https://fix-outlet-animation-config.effex-api.pages.dev

View logs

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying effex with  Cloudflare Pages  Cloudflare Pages

Latest commit: 275826e
Status: ✅  Deploy successful!
Preview URL: https://11e866ea.effex.pages.dev
Branch Preview URL: https://fix-outlet-animation-config.effex.pages.dev

View logs

@jonlaing
jonlaing merged commit 2ea19e4 into main Aug 7, 2026
4 checks passed
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.

1 participant