feat(react,vue,solid): add closed and loading to route views - #106
Draft
sergeysova wants to merge 12 commits into
Draft
feat(react,vue,solid): add closed and loading to route views#106sergeysova wants to merge 12 commits into
sergeysova wants to merge 12 commits into
Conversation
|
|
🦋 Changeset detectedLatest commit: e40e675 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
✅ Deploy Preview for effector-router ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Adds failing specs for the upcoming `otherwise` and `loading` properties of `createRouteView`/`createLazyRouteView` in every web binding: a closed route renders `otherwise`, a pending route renders `loading`, an opened sibling still wins over any declared fallback, fallbacks render through `Outlet`, they are wrapped by the view layout (including `withLayout` groups), and `loading` doubles as the lazy chunk fallback.
`createRouteView`/`createLazyRouteView` accept two new components: - `otherwise` — rendered while the route is not opened; - `loading` — rendered while the route is pending, and used as the lazy chunk fallback unless the chunk-only `fallback` is declared. Both take part in the routes view / `Outlet` selection: an opened view always wins, and only when nothing is opened the last declared fallback renders, with a pending `loading` taking precedence over a closed `otherwise`. Fallbacks are wrapped by the same layout the view uses, including `withLayout` groups, so a grouped layout stays mounted while a nested chain resolves — the skeleton pattern that `atomic-router`'s nested route views used to cover. Each binding gained a mirrored internal `resolve-route-view` module owning the fallback wrapping and the resolution; `createRoutesView` and `Outlet` now render through it.
Covers the new properties in the React, Vue, and Solid reference pages for createRouteView/createLazyRouteView — including the selection order used by createRoutesView and Outlet, the layout wrapping, and the caveat about a per-view otherwise shadowing the routes-view not-found screen. Also notes the pending-state skeleton in the chainRoute reference, records that native navigators render `view` only, and adds the mirrored resolve-route-view module to AGENTS.md.
sergeysova
force-pushed
the
claude/createroute-otherwise-loading-h1g5nw
branch
from
July 29, 2026 11:29
8b8e72d to
f816d2b
Compare
Locks the contract that removes the routes-view `otherwise` flash: the lazy `fallback` component also covers the pending route, not just the chunk request, and `loading` wins when both are declared.
`fallback` only covered the chunk request, so a lazy view whose route was still pending fell through to the routes view `otherwise` — the not-found screen flashed for a moment before the page appeared. `loading` covers both waits, and `fallback` now behaves as its alias: it is used when `loading` is absent, and it also renders while the route is pending. The type is marked `@deprecated` so editors point at `loading`.
Rewrites the lazy route view examples and property tables around `loading`, documents `fallback` as its deprecated alias, and notes that covering both waits with one component is what stops the routes view `otherwise` from flashing.
…osed
`closed` is the exact complement of `opened` and matches the router's own
lifecycle vocabulary, so a view now reads as its three states: `view` when
opened, `loading` while pending, `closed` while closed.
It also removes the clash with `createRoutesView({ otherwise })`, which keeps
its name and its different meaning — nothing matched the URL. That prop is
untouched.
Answers the question this feature came from: how to keep a skeleton per nesting level while a chain of routes prepares, instead of letting the not-found screen flash between navigations. The guide is task-oriented, so it lives in the how-to quadrant, which until now was an empty placeholder — it is now listed in the sidebar and the top nav. Every snippet is one runnable file, docs/how-to/nested-loading-skeletons.tsx, executed by packages/react/tests/how-to-nested-loading.test.tsx: the test drives three deferred effects and asserts what is on screen at each stage, so the documented result is the tested one.
The resolved selection was a fresh object on every router update, so any change in the pending flags — a sibling chain starting to prepare — re-rendered the selected view and everything under it. The resolution now keeps its identity while the same view and component stay selected, and both renderers sit behind a memo boundary, so unrelated updates stop there. The outlet context value is memoized per view as well. Vue and Solid already behaved this way; all three suites now assert it.
Collects the shapes the same pieces take for adjacent goals: filling an empty slot, keeping the frame while only the inner part swaps, avoiding the remount a component swap costs, streaming a ready child inside a parent skeleton, holding a transition before the URL commits so the current page stays, redirecting instead, splitting a level into a chunk, and grouping a layout. The two claims that are not obvious from the API — an `<Outlet />` inside `loading`, and `beforeNavigate` keeping the previous page on screen — are covered by tests next to the guide's own.
Applies the review suggestion: the table already shows the sequence, and the mechanism section states the not-found behaviour where it belongs.
Two guarantees the suite did not hold before, both found while investigating layout remounts: - switching pages inside one `withLayout` group keeps the layout instance, and moving to another group swaps it exactly once; - the frame between a closing route and the opening one belongs to the target's `loading`, so the not-found screen never appears mid-navigation. Both depend on the target declaring `loading`: that frame has nothing opened, and without a fallback the routes view falls through to `otherwise` and takes the layout down with it.
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
Brings back the
atomic-routerskeleton pattern (nestedCreateRouteViewwithotherwise) in the shape this router uses — as two new properties ofcreateRouteView/createLazyRouteViewin the React, Vue, and Solid bindings:closed— rendered while the route is not opened;loading— rendered while the route is pending (beforeOpeneffects, achainRoutepreparation) and while a lazy chunk loads.A view now reads as its three states:
viewwhen opened,loadingwhile pending,closedwhile closed.createRoutesView({ otherwise })keeps its name and its different meaning — nothing matched the URL.createRoutesViewandOutletstill render a single view. Resolution order:loadingof a pending view;closedof a closed view;otherwiseprop ofcreateRoutesView(nullinside anOutlet).Because an opened view always wins,
loadingnever replaces a page already on screen. Fallbacks are wrapped by the view'slayoutand by itswithLayoutgroup, so a grouped layout stays mounted while a nested chain resolves — the parent view keeps rendering while itsOutletshows the child's skeleton.This also removes the flash of the routes-view
otherwise(the not-found screen) that appeared for a split second while a lazy view's chunk or chained data was loading:createLazyRouteView'sfallbackonly covered the chunk request, so a pending route fell through to the not-found screen.fallbackis deprecated in favour ofloading, which covers both waits, and keeps working as its alias.Each binding gained a mirrored internal
resolve-route-viewmodule that owns the fallback wrapping and the resolution;createRoutesViewandOutletnow render through it.useOpenedViewskeeps its current public semantics (opened views only).React Native re-exports the React factories, so the props type-check there; the
@react-navigationnavigators mount every route view as a screen and renderviewonly — documented in the React Native reference.Type of change
Checklist
pnpm buildpassespnpm typecheckpasses (pluspnpm verify:typecheck-standalone,pnpm :docs typecheck,pnpm :docs check-links)pnpm testpasses and I added/updated tests for my change (new specs across the three bindings; Solid example e2e also run against a local Chromium)pnpm lintpassesminorfor@effector/router-react,@effector/router-vue,@effector/router-solid)