Skip to content

refactor: give each app page its own data scope - #289

Merged
ruchamahabal merged 16 commits into
frappe:developfrom
ruchamahabal:fix-render
Oct 8, 2026
Merged

ruchamahabal merged 16 commits into
frappe:developfrom
ruchamahabal:fix-render

Conversation

@ruchamahabal

@ruchamahabal ruchamahabal commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Before

A rendered app keeps the data of the current page in one shared store. When the user goes to a different page, two things go wrong:

pr289-before.mp4

After

pr289-after.mp4

Each page now has its own data. The router prepares a page before it shows it.

  • page/pageScope.ts creates one scope for one page. A scope has the variables, the resources and the page script of that page. The code is the old codeStore body, moved without changes.
  • page/pageLoader.ts runs in a beforeResolve guard. It fetches the page, creates its scope, loads the script and builds the block tree. The page that the user leaves stays on the screen and keeps its data until the new page is ready.
  • AppPage.vue replaces AppContainer.vue. One instance shows one page. It takes the prepared page, provides the scope to its blocks and tears the scope down when it unmounts.
  • Each page keeps its own route. The route follows param changes on the same page only. A page that the user leaves never reads the next page's params.
  • usePageScope() gives a component the scope of its page. In the editor it gives the editor's one scope from codeStore.
  • Preview reload is now a forced router.replace of the same URL. The page mounts again with a fresh scope.
  • The dev server's renderer is now the only renderer on a developer-mode site. A Vite hook keeps the built script tag inside the Jinja condition.

Behavior

  • Navigation to a different page: the old page stays until the new page is ready. Then they swap in one render.
  • Navigation to the same page with new params: the page stays mounted. Its resources fetch again with the new params.
  • A navigation that the user cancels: the prepared page is discarded.
  • A page that fails to load: the router shows Not Found at that URL.
  • The app's router.ts hooks run before Studio's guards, as before.

Closes #284, Closes #287

…ge's params

Resource watchers ran before AppContainer's route watcher, so leaving
/category/:category for / refetched with category=undefined. Stop them
in a beforeEach guard when the destination is a different page.

Fixes frappe#284
The beforeEach guard stopped resource watchers before the navigation was
confirmed, so a cancelled navigation left the current page's resources
dead. A sync route watcher runs loadPage's teardown before the resource
watchers flush, and only once the route has actually changed.
getEvaluatedFilters sets a filter to undefined on purpose so it is dropped
from the request, which Filters does not allow.
Page data (variables, resources, page script and the contexts bindings
evaluate against) lived in the code store, a singleton, so there could
only ever be one page's data at a time.

Move it into createPageScope(), which returns one page's data as a
reactive object. The code store now holds the route, the router and the
active scope, and components read page data through usePageScope().
Page script hot updates go to the active scope.

There is still a single scope that each page load resets in place, so
behaviour is unchanged.
A page switch reset the single page scope while the old blocks were
still on screen, so they re-rendered against empty data: the app flashed
blank between pages and threw on bindings like rows.map.

The app renderer now loads the next page into a new scope and activates
it in the same tick as the block swap, so the page on screen keeps its
own data until it is replaced. The new page's script and resources also
set up against their own scope instead of the outgoing page's, and a
failed or superseded load is torn down without touching the active page.

Fixes frappe#287
@greptile-apps

greptile-apps Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[High risk] Refactors page data isolation and lifecycle management.

The PR appears safe to merge; no new actionable issue was established.

Reviews (13) · Last reviewed commit: "perf: mount a published page from its ca..." · Reviewed by Greptile

Comment thread frontend/src/stores/codeStore.ts Outdated
Comment thread frontend/src/stores/codeStore.ts Outdated
Comment thread frontend/cypress/component/data-source-fetching.cy.ts Outdated
setPageVariables, setPageResources and teardownPage moved from the code
store to the page scope.
Comment thread frontend/src/page/pageScope.ts
A router beforeResolve guard (pageLoader.ts) builds the next page's scope
before the navigation completes, so the page being left stays untouched
and the new one mounts with every binding resolved. AppPage provides its
scope and pins its own route, so leaving a page never refetches with the
next page's params. The code store is now editor-only.

Fixes frappe#284, fixes frappe#287
Reading the preview window's location is cross-origin in dev, and a
reused named tab is reloaded in the background without focus.
Comment thread frontend/src/router/pageLoader.ts Outdated
Comment thread frontend/src/router/pageLoader.ts Outdated
Comment thread frontend/src/utils/useLivePreview.ts Outdated
- navigating again while a page is still loading no longer breaks the new page
- a page that fails to load frees its data sources
- a preview reload is skipped if you already moved to another page
Comment thread frontend/src/stores/studioStore.ts Outdated
loadPage and discardPage for the router guards, useLoadedPage for AppPage.
The pinned-route watcher moves into useLoadedPage with the handoff.
Neither is a store or a router concern: both describe one rendered page.
Vite hoists renderer.html's built script tag out of the Jinja else-branch, so a
developer-mode site ran the built renderer and the dev server's renderer together
and flashed the error page on reload. A post transformIndexHtml hook wraps the
tag in the Jinja condition again.
Comment thread frontend/src/utils/helpers.ts Outdated
The first visit fetches the definition. Later visits mount from the cache at
once and refresh it in the background, so a republished page shows on the
next visit. The preview keeps fetching so draft saves show.
@ruchamahabal
ruchamahabal merged commit c599e33 into frappe:develop Oct 8, 2026
3 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.

Page switch renders the old page with an empty context A page being left refetches its resources with the next page's route params

1 participant