Edit post: consume preload cache before React mount#78508
Conversation
Alternative to the selector-keyed hydration in #78505. Keeps the existing path-based createPreloadingMiddleware in place and instead resolves a known list of selectors at boot time. Each apiFetch short-circuits via the middleware, so no network traffic happens; the resolvers run their own dispatch / fan-out / shorthand-alias chains end-to-end, populating the store from a single source of truth. React mount is deferred until the kickoff Promise.all settles, so the first render finds resolved metadata and never triggers a setTimeout(0) resolution dance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Size Change: +975 B (+0.01%) Total Size: 7.98 MB 📦 View Changed
ℹ️ View Unchanged
|
- @wordpress/api-fetch: createPreloadingMiddleware exposes
__unstableClear, and the package exposes
apiFetch.__unstableClearPreloadedData() that walks installed
middlewares and clears each. Lets the editor drop any preload
entries the kickoff didn't consume.
- @wordpress/edit-post: preloadResolutions is now async and runs in
two phases. Phase 1 awaits the selectors whose args we know from
PHP (post id + type). Phase 2 reads phase-1 state to derive args
for resolvers that depend on them — the user-global-styles id
comes from __experimentalGetCurrentGlobalStylesId; the
default-template slug from the post's slug + post_type. After
both phases settle, apiFetch.__unstableClearPreloadedData() runs,
so anything we missed falls through to a real network request
and surfaces in the preload e2e snapshot test.
Also adds the forwardResolver alias kickoffs the editor actually
uses (getThemeSupports → getCurrentTheme, getPostType →
getEntityRecord('root','postType',...), getEditedEntityRecord →
getEntityRecord('postType',...)) so each alias's resolution
metadata is marked finished even though they share the underlying
resolver work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Preloaded entries no longer self-delete on first read. The previous single-use behavior was guarding against stale-data leakage on subsequent requests, but the editor now explicitly clears the cache via `__unstableClearPreloadedData()` just before React mounts — so the boundary that the deletes enforced has moved to an explicit call. Multi-use within the boot window lets a single preloaded URL satisfy multiple selectors that would otherwise each fire `apiFetch` for the same path. The two duplicate-fetches the post-editor snapshot test previously listed as "to do" (`GET /wp/v2/taxonomies?context=view` and `OPTIONS /wp/v2/settings`) are now served from cache; the snapshot drops both lines. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
I love this idea. It's similar to how TanStack Router/Query data can be preloaded in the route handler, to avoid too many intermediate states and loading indicators in the actual React UI. It would be nice to have some dev mode reporting mechanism that would help us to maintain the coordination between:
We could detect cases where:
I'm curious why so many e2e tests are failing on this. |
Restores single-use semantics as the default for the preloading middleware (its trunk behavior — entries self-delete on first read). Adds two new hooks exposed via @wordpress/api-fetch's privateApis: - enablePreloadMultiUse(): flips installed middlewares into multi-use mode so a single preloaded URL can back multiple selectors. Used by the edit-post bootstrap, where a single OPTIONS /wp/v2/settings has to satisfy three selectors (getEntitiesConfig, canUser, and getEntityRecord for the site entity). - clearPreloadedData(): drops every remaining cache entry. Edit-post calls this just before React mounts so anything the kickoff resolveSelects didn't consume falls through to a real network request (and surfaces in tests / DevTools). Site editor and other consumers keep the existing single-use behavior — no test snapshot changes outside of post-editor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| [ | ||
| `GET /wp/v2/comments?context=edit&post=${ postId }&type=note&status=all&per_page=100`, | ||
| 'GET /wp/v2/taxonomies?context=edit&per_page=100', | ||
| 'GET /wp/v2/taxonomies?context=view', |
There was a problem hiding this comment.
You discovered that these preloads are not used any more?
There was a problem hiding this comment.
These are no longer requested because they're present in preload data. But different resolvers were calling them before causing the second one to miss the cache. Doesn't happen anymore after making cache multi use until the editor mounts
…th symbols Each `createPreloadingMiddleware` instance previously exposed its multi-use and clear hooks as `__unstable*` string properties. Symbols keep the same surface fully private to the package — they're not enumerable, not discoverable via `Object.keys`, and unreachable unless a caller imports the symbol from the module. The two callers in `api-fetch/src/index.ts` (and the unit tests) now use the symbols directly; nothing outside the package can call them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| enable(); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
I wish we had a better way to call functions on the middleware. The middleware is created by an inline script generated by the server-side PHP script. The code is:
wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( data ) );Somewhere there is the actual middleware object on which we could call the methods, but unfortunately we don't have access to it. It was passed to use() and forgotten.
There was a problem hiding this comment.
Yeah, I think this is the best we can do 😔
I can build these into the preload e2e tests:
|
| * `wp.apiFetch.use( something )` lookup — the only callers that can | ||
| * reach them are the ones inside this package that import the symbols. | ||
| */ | ||
| export const ENABLE_MULTI_USE = Symbol( 'preloadingEnableMultiUse' ); |
There was a problem hiding this comment.
Decided it's best if we keep things backwards compatible and opt-in.
The start-page and start-template "Choose a pattern" modals open only when the post looks fresh (`! dirty && empty`). They were checking `isEditedPostDirty` / `hasEditsForEntityRecord`, both of which return true while `isSavingEntityRecord` is in flight. At boot the CRDT sync manager dispatches a phantom save on the just-received post record (no actual edits), and once the preload kickoff makes that happen before mount, the modal sees `dirty: true` and never opens. Read `getEntityRecordNonTransientEdits` directly so the freshness check ignores in-flight saves and only fires on actual user edits. The save guard semantics of `hasEditsForEntityRecord` are unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Flaky tests detected in 536d38d. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/26237998441
|
|
Warning: Type of PR label mismatch To merge this PR, it requires exactly 1 label indicating the type of PR. Other labels are optional and not being checked here.
Read more about Type labels in Gutenberg. Don't worry if you don't have the required permissions to add labels; the PR reviewer should be able to help with the task. |
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
The preloading middleware now tracks `${ method } ${ path }` tags that
were never served (`unusedGet` / `unusedOptions`). On `CLEAR` it warns
if any are left over, or logs a one-line success otherwise. This makes
two kinds of drift visible to anyone running the editor:
- entries the server preloads via `block_editor_rest_api_preload_paths`
that no one fetches (warn — drop them or wire them into the kickoff),
- the all-good case (log — nothing to do).
`createPreloadingMiddleware` destructures the input into a top-level
`{ OPTIONS, ...GET }` so the GET vs OPTIONS branching only exists at
the entry point; downstream hits and `CLEAR` no longer walk a nested
shape.
The post-editor preload spec consumes both signals: it listens for the
`[api-fetch][preload] …` line as a mount-boundary marker, asserts
`requestsUntilMount` contains only the two unavoidable collab POSTs
(CRDT save + initial wp-sync poll), and asserts the marker is the
success line. Together they catch both new uncached kickoff requests
and preload entries no one consumes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2b32999 to
f5ebd3e
Compare
Same workaround on both — keep the explanation identical. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| coreSelect.__experimentalGetCurrentGlobalStylesId(); | ||
| if ( globalStylesId ) { | ||
| tasks.push( | ||
| core.getEntityRecord( 'root', 'globalStyles', globalStylesId ), |
There was a problem hiding this comment.
This getEntityRecord is conditional depending on whether the current user can update the settings or not. When read-only, there is additional context=view query param, overriding the default context=edit. It's visible in useGlobalStyles().
This PR addresses four things at once:
/wp/settingsfor two distinct resolvers.How do we make sure that selectors are added when adding preload paths?
The current preload tests would catch this. Preload cache is cleared after load, which means the path would show up as not preloaded in the test, even if the preload path is there server side. Additionally a warning will be logged (which we could eventually also test for).