Skip to content

Commit 0eb6ce2

Browse files
committed
fix(sidebar): hydrate collapse state before paint to stop refresh flash
The collapsed sidebar swaps entire subtrees (collapsed flyout vs expanded lists), but isCollapsed only resolved after the first paint via auto rehydration, so a collapsed reload rendered the expanded tree into the 51px rail and then reflowed — the misplaced/flashing content on refresh. Adopt zustand's documented SSR pattern: skipHydration on the persist config (first render keeps the default false, matching SSR HTML) and flush persist.rehydrate() from a useLayoutEffect so the correct structure commits in the same pre-paint frame. Removes the old race where onRehydrateStorage lifted the data-sidebar-collapsed mask before React committed the rail.
1 parent 1d0fee9 commit 0eb6ce2

2 files changed

Lines changed: 19 additions & 0 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,15 @@ export const Sidebar = memo(function Sidebar() {
382382
const toggleCollapsed = useSidebarStore((state) => state.toggleCollapsed)
383383
const isOnWorkflowPage = !!workflowId
384384

385+
// Hydrate the persisted sidebar state before the browser paints. The store
386+
// sets `skipHydration` so its default (`isCollapsed: false`) matches the SSR
387+
// HTML on first render; flushing rehydration here re-renders the correct
388+
// collapsed/expanded structure synchronously in the same pre-paint commit,
389+
// preventing the expanded tree from flashing inside the collapsed rail.
390+
useLayoutEffect(() => {
391+
void useSidebarStore.persist.rehydrate()
392+
}, [])
393+
385394
const isCollapsedRef = useRef(isCollapsed)
386395
useLayoutEffect(() => {
387396
isCollapsedRef.current = isCollapsed

apps/sim/stores/sidebar/store.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,16 @@ export const useSidebarStore = create<SidebarState>()(
5858
}),
5959
{
6060
name: 'sidebar-state',
61+
/**
62+
* Hydration is driven manually from a `useLayoutEffect` (see Sidebar) so it
63+
* runs synchronously before the first paint. Auto-hydration would either
64+
* (a) run at module load and make the client's first render disagree with
65+
* the server's `isCollapsed: false` HTML — a mismatch React recovers from by
66+
* flashing the server tree, or (b) run after paint, reflowing the expanded
67+
* tree into the collapsed rail. Skipping it lets the layout effect flip the
68+
* structure in the same pre-paint commit, so neither flash is visible.
69+
*/
70+
skipHydration: true,
6171
onRehydrateStorage: () => (state) => {
6272
if (state) {
6373
state.setHasHydrated(true)

0 commit comments

Comments
 (0)