payloadcms / payloadcms/payload
Live preview closes itself mid-edit: aborted form-state request clears livePreviewURL
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 44.8k
- Forks
- 4.2k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 53
Description
Describe the Bug
With live preview enabled and autosave on a short interval, the live-preview pane closes itself while the user is typing and its toggle button disappears with it. It does not come back without a full page reload. Nothing is logged — no client error, no server error, every request is a 200 — which makes it very hard to attribute.
The cause is an aborted form-state request being applied as though it were a successful response.
-
Every
onSavebegins by aborting the previous one:// packages/ui/src/views/Edit/index.tsx const controller = handleAbortRef(abortOnSaveRef)So a save that lands while the previous save's form-state request is still in flight aborts that request.
-
The aborted call resolves to
{ state: null }— note the abort is checked, not thrown, so nothing reaches thecatchand nothing is logged:// packages/ui/src/providers/ServerFunctions/index.tsx — getFormState if (!remoteSignal?.aborted) { const result = await serverFunction({ name: 'form-state', args: { ... } }) if (!remoteSignal?.aborted) { return result } } // ... return { state: null } // no `livePreviewURL` key -
The Edit view destructures
livePreviewURLoff that result and applies it unconditionally:// packages/ui/src/views/Edit/index.tsx const { livePreviewURL, previewURL, state } = await getFormState({ /* ... */ signal: controller.signal }) if (isLivePreviewEnabled && typeofLivePreviewURL === 'function') { setLivePreviewURL(livePreviewURL) // undefined on an aborted request } -
The provider treats a falsy URL as "turn live preview off":
// packages/ui/src/providers/LivePreview/index.tsx — setLivePreviewURL if (!incomingURL) { setIsLivePreviewing(false) } if (incomingURL !== url) { setAppIsReady(false) setURL(incomingURL) } -
And
LivePreviewTogglerhides itself when the context URL is falsy, so the user loses the button as well as the pane:// packages/ui/src/elements/LivePreview/Toggler/index.tsx if (!livePreviewURL) { return null }
Nothing sets isLivePreviewing back to true, so the only recovery is reloading the page.
An aborted request means "this result is stale, ignore it", but step 3 reads it as "the server says there is no preview URL". Guarding the apply on a successful response — or having getFormState signal abortion distinguishably from a real empty result — would fix it.
Server-side is not the trigger. I instrumented our livePreview.url function: it returns a valid string on every invocation, including the ones immediately before the drop-out. buildFormState also only assigns res.livePreviewURL when truthy, so a url function returning null correctly leaves the previous URL in place. It is specifically the client applying an aborted result that clears it.
Link to the code that reproduces this issue
https://github.com/payloadcms/payload/tree/main/templates/website
The official website template reproduces this unmodified — it ships a 100ms autosave interval on both Pages and Posts:
versions: {
drafts: {
autosave: {
interval: 100, // We set this interval for optimal live preview
},
schedulePublish: true,
},
That comment is worth flagging on its own: at 100ms the interval reliably breaks the feature it says it is optimising. Form-state on our site takes 20–460ms, so saves overlap more or less continuously.
Reproduction Steps
pnpx create-payload-app@latest -t website- Open any Page in the admin and enable live preview.
- Add enough blocks that a form-state round-trip takes longer than the autosave interval — a heavy page makes this immediate, but it can also be forced with DevTools network throttling or by adding an artificial delay to a field hook.
- Type continuously into a field for a few seconds.
Expected: the live-preview pane stays open and keeps refreshing.
Actual: the pane closes on its own and the toggle button disappears. No console or server error. It stays gone until the page is reloaded.
A reliable tell that it has happened, if you are watching the server log: POST /api/payload-preferences/collection-<slug> fires — from the provider's useEffect on isLivePreviewing writing editViewType: 'default' — at exactly the point the preview's page refreshes stop.
Raising the interval (we moved to 2000) makes the race very unlikely but does not close it: any two saves that overlap a slow enough form-state still hit it.
Which area(s) are affected?
area: live-preview, area: ui, area: templates
Environment Info
Binaries:
Node: 22.22.3
npm: 10.9.8
pnpm: 10.29.3
Relevant Packages:
payload: 3.85.1
next: 16.2.6
@payloadcms/db-mongodb: 3.85.1
@payloadcms/live-preview: 3.85.1
@payloadcms/live-preview-react: 3.85.1
@payloadcms/next/utilities: 3.85.1
@payloadcms/ui/shared: 3.85.1
react: 19.2.6
react-dom: 19.2.6
Operating System:
Platform: darwin
Arch: arm64
Available memory (MB): 65536
Available CPU cores: 10
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with getFormState in packages/ui/src/providers/ServerFunctions/index.tsx and its caller in packages/ui/src/views/Edit/index.tsx, then trace setLivePreviewURL in packages/ui/src/providers/LivePreview/index.tsx and LivePreviewToggler. Reproduce using templates/website with the 100ms autosave interval while typing into a heavy page. Done means an aborted form-state request no longer closes the live-preview pane or hides its toggle, while genuine preview updates still work.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- nextjs, react, typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100