fix: Stale canvas events shown when switching between canvases without page refresh#3506
Merged
forestileao merged 2 commits intomainfrom Mar 24, 2026
Merged
fix: Stale canvas events shown when switching between canvases without page refresh#3506forestileao merged 2 commits intomainfrom
forestileao merged 2 commits intomainfrom
Conversation
When navigating between canvases without a page refresh, the UI displayed stale event data from the React Query cache. The root cause was twofold: 1. The global staleTime of 5 minutes meant cached canvas data was served without triggering a background refetch. 2. The hasInitializedStoreRef guard prevented the node execution store from re-initializing when a background refetch eventually completed with fresh data. Changes: - Set staleTime: 0 on useCanvas and useCanvasEvents hooks so cached data always triggers a background refetch when the query key changes. - Guard store initialization with !canvasFetching to skip populating the store from stale cached data while a refetch is in progress. - Reset hasInitializedStoreRef and clear the node execution store when canvasId changes, allowing the store to re-initialize from the fresh fetch result. Closes #3504
|
👋 Commands for maintainers:
|
Collaborator
|
/sp start |
|
✅ Ready. Web: https://pr-3506-ephemeral.superplane.com |
Collaborator
|
@cursoragent there is an UI thing that I don't like. The first node events are appearing after the nodes loaded. After the "Loading..." |
Contributor
Author
|
Revised the approach to eliminate the loading gap the reviewer noticed. The new strategy:
|
Collaborator
|
/sp stop |
|
✅ Ephemeral machine has been terminated. |
Replace the blocking approach (clear store + skip init during refetch) with an immediate-init + re-init pattern: - Initialize the store immediately from cached data so nodes render with their events right away (no Loading... flash). - Track a pendingStoreReinitRef flag that allows one re-initialization when the background refetch completes with fresh data. - Remove clearNodeExecutionStore() from the canvas switch effect since initializeFromWorkflow already replaces all store data.
EtnDiaz
pushed a commit
to EtnDiaz/superplane
that referenced
this pull request
Mar 28, 2026
…t page refresh (superplanehq#3506) <!-- CURSOR_AGENT_PR_BODY_BEGIN --> ## Summary Fixes superplanehq#3504 When navigating between canvases without a page refresh, the UI displayed stale event data from the React Query cache instead of the latest state. WebSocket updates continued to work correctly, but the initial data rendered came from the cached canvas status response, which was outdated. ## Root Cause The issue had two contributing factors: 1. **Global `staleTime` of 5 minutes** — The React Query client is configured with a global `staleTime: 5 * 60 * 1000`. This meant cached canvas detail data (which includes `status` with `lastExecutions`, `nextQueueItems`, `lastEvents`) was considered "fresh" and served without triggering a background refetch when navigating back to a previously visited canvas. 2. **Store initialization guard** — The `hasInitializedStoreRef` in the workflow page prevented the node execution Zustand store from re-initializing after a background refetch completed with fresh data. Once the store was initialized from stale cache, it stayed stale. ## Changes ### `web_src/src/hooks/useCanvasData.ts` - Set `staleTime: 0` on `useCanvas` and `useCanvasEvents` hooks to override the global 5-minute default. This ensures a background refetch is always triggered when the query key changes (i.e., when `canvasId` changes), so fresh data is fetched on every canvas switch. ### `web_src/src/pages/workflowv2/index.tsx` - **Immediate init + deferred re-init**: When switching canvases, the store initializes immediately from cached data (so nodes render with their events right away — no loading gap). A `pendingStoreReinitRef` flag then allows exactly one re-initialization when the background refetch completes with fresh data. - **Reset on canvas switch**: In the `canvasId` change effect, `hasInitializedStoreRef` is reset and `pendingStoreReinitRef` is set to `true`, enabling the re-initialization cycle for the incoming canvas. ## How It Works 1. User navigates from Canvas B to Canvas A 2. `canvasId` change effect resets `hasInitializedStoreRef = null` and sets `pendingStoreReinitRef = true` 3. React Query serves cached Canvas A data with `isFetching: true` (background refetch triggered due to `staleTime: 0`) 4. Store initialization effect: ref mismatch → **initializes immediately from cached data** (no loading gap); since `canvasFetching` is `true`, `pendingStoreReinitRef` stays `true` 5. Background refetch completes: fresh Canvas A data arrives, `canvasFetching` becomes `false` 6. Store initialization effect: `pendingStoreReinitRef` is `true` and not fetching → **re-initializes from fresh data**, clears the flag 7. WebSocket events continue to merge correctly with the fresh baseline <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-f749f273-d24d-402e-b25e-9224f558c054"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/background-agent?bcId=bc-f749f273-d24d-402e-b25e-9224f558c054"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
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
Fixes #3504
When navigating between canvases without a page refresh, the UI displayed stale event data from the React Query cache instead of the latest state. WebSocket updates continued to work correctly, but the initial data rendered came from the cached canvas status response, which was outdated.
Root Cause
The issue had two contributing factors:
Global
staleTimeof 5 minutes — The React Query client is configured with a globalstaleTime: 5 * 60 * 1000. This meant cached canvas detail data (which includesstatuswithlastExecutions,nextQueueItems,lastEvents) was considered "fresh" and served without triggering a background refetch when navigating back to a previously visited canvas.Store initialization guard — The
hasInitializedStoreRefin the workflow page prevented the node execution Zustand store from re-initializing after a background refetch completed with fresh data. Once the store was initialized from stale cache, it stayed stale.Changes
web_src/src/hooks/useCanvasData.tsstaleTime: 0onuseCanvasanduseCanvasEventshooks to override the global 5-minute default. This ensures a background refetch is always triggered when the query key changes (i.e., whencanvasIdchanges), so fresh data is fetched on every canvas switch.web_src/src/pages/workflowv2/index.tsxpendingStoreReinitRefflag then allows exactly one re-initialization when the background refetch completes with fresh data.canvasIdchange effect,hasInitializedStoreRefis reset andpendingStoreReinitRefis set totrue, enabling the re-initialization cycle for the incoming canvas.How It Works
canvasIdchange effect resetshasInitializedStoreRef = nulland setspendingStoreReinitRef = trueisFetching: true(background refetch triggered due tostaleTime: 0)canvasFetchingistrue,pendingStoreReinitRefstaystruecanvasFetchingbecomesfalsependingStoreReinitRefistrueand not fetching → re-initializes from fresh data, clears the flag