Skip to content

fix(zustand): v5 selector stability issues#4539

Merged
icecrasher321 merged 2 commits intostagingfrom
fix/zustand-v5-compat
May 9, 2026
Merged

fix(zustand): v5 selector stability issues#4539
icecrasher321 merged 2 commits intostagingfrom
fix/zustand-v5-compat

Conversation

@icecrasher321
Copy link
Copy Markdown
Collaborator

@icecrasher321 icecrasher321 commented May 9, 2026

Summary

  • Fixes Zustand v5 selector stability regressions that could trigger infinite render loops during workflow loading.
  • Tightens store subscriptions to avoid whole-store rerenders and unstable derived selector references.
  • Adds a CI audit for unsafe Zustand v5 selector patterns to prevent regressions.

Type of Change

  • Bug fix

Testing

Tested manually

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel
Copy link
Copy Markdown

vercel Bot commented May 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment May 9, 2026 11:05pm

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 9, 2026

PR Summary

Medium Risk
Touches multiple Zustand selectors and store access patterns in workflow UI, which can affect render frequency and state updates during workflow loading. Adds a new CI gate that can fail builds if selector patterns are flagged, so false positives could block merges.

Overview
Prevents Zustand v5 selector instability in the workflow UI by tightening store subscriptions (favoring field-level selectors) and wrapping multi-field selections in useShallow / useStoreWithEqualityFn to avoid fresh object/array allocations that can trigger rerender loops.

Adds stable empty fallback constants for sub-block values (EMPTY_SUBBLOCK_VALUES, EMPTY_BLOCK_SUBBLOCK_VALUES) and updates call sites to use them instead of inline {} defaults.

Introduces a new check:zustand-v5 script plus a CI step to audit unsafe selector patterns, and updates contract/codegen scripts to run generated output through a shared Biome formatter helper for consistent diffs.

Reviewed by Cursor Bugbot for commit 8ca96c2. Configure here.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 9, 2026

Greptile Summary

This PR addresses Zustand v5 selector stability regressions that could cause infinite render loops during workflow loading, by tightening store subscriptions across several components and adding a CI script to prevent future regressions.

  • Selector fixes: Whole-store destructuring calls (useWorkflowRegistry(), useWorkflowStore(...)) are replaced with field-specific selectors or useShallow-wrapped selectors; inline {} and [] fallbacks are replaced with module-level stable constants (EMPTY_SUBBLOCK_VALUES, EMPTY_BLOCK_SUBBLOCK_VALUES, EMPTY_VARIABLES).
  • CI audit: scripts/check-zustand-v5-selectors.ts is a new static analysis script that detects unsafe patterns (whole-store subscriptions, fresh object/array returns, unstable fallback literals) via regex and paren-matching, with a // zustand-v5-safe: <reason> escape hatch for intentional exceptions.
  • Tooling: scripts/format-generated-source.ts centralizes biome formatting for generated files, and all sync scripts now run their output through it (driving the bulk key-reformatting in tool-schemas-v1.ts).

Confidence Score: 5/5

Safe to merge — all application changes are targeted subscription narrowing with no behavioral side-effects, and the new CI script is additive.

Each component change is a mechanical narrowing of an existing subscription: replacing whole-store destructuring with a field selector, or wrapping a returned object with useShallow for shallow comparison. The module-level empty constants correctly fix the root cause of reference instability. The useShallow(useCallback(..., [blockId])) composition is valid: useCallback memoizes on [blockId], and useShallow's internal useCallback depends on the outer selector reference, so the wrapped function only changes when blockId changes. No logic paths were altered, only re-render trigger conditions.

No files require special attention. The check-zustand-v5-selectors.ts script has minor edge cases around template-literal interpolation tracking and .reduce() false positives that have already been discussed on the PR.

Important Files Changed

Filename Overview
scripts/check-zustand-v5-selectors.ts New 371-line CI script that statically audits Zustand v5 selector patterns; logic is sound with regex-based pattern matching and several bypass heuristics, though template-literal ${} interpolations inside selectors are not tracked during paren-matching (minor edge case).
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx Wraps the multi-field useWorkflowStore selector with useShallow(useCallback(...)) for stable shallow comparison; splits two destructured whole-store calls into field-specific selectors.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx Replaces unstable getter-call pattern (getVariablesByWorkflowId) with useStoreWithEqualityFn + isEqual deep comparison for derived variable list; adds module-level EMPTY_VARIABLES constant to avoid fresh array references.
apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-folder-expand.ts Extracts stable Zustand actions (toggleExpanded, setExpanded) to module scope via getState(); hook now subscribes only to expandedFolders, eliminating two unnecessary store subscribers.
apps/sim/stores/workflows/subblock/store.ts Adds EMPTY_BLOCK_SUBBLOCK_VALUES module-level constant to companion EMPTY_SUBBLOCK_VALUES, providing a stable reference for block-level ?? fallbacks in selectors.
apps/sim/components/emcn/components/badge/badge.tsx Converts Badge to forwardRef, adds displayName, and narrows the React import to named imports only.
scripts/format-generated-source.ts New helper that pipes generated source through biome format via spawnSync; used by sync scripts to ensure generated files are consistently formatted.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Component renders] --> B{Zustand store hook call}
    B -->|Whole-store subscription before PR| C[Subscribe to entire store]
    B -->|Field-specific selector after PR| D[Subscribe to only needed fields]
    C --> E[Any state change triggers re-render]
    D --> F{Selector returns fresh object/array?}
    F -->|Yes - BEFORE fix| G[New reference every render - re-render loop]
    F -->|No - stable primitive or useShallow-wrapped| H[Shallow equality check - skip re-render if same values]
    H --> I[Stable render]
Loading

Reviews (2): Last reviewed commit: "address comments" | Re-trigger Greptile

Comment thread scripts/check-zustand-v5-selectors.ts
Comment thread scripts/check-zustand-v5-selectors.ts
@icecrasher321
Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321
Copy link
Copy Markdown
Collaborator Author

bugbot run

@icecrasher321 icecrasher321 merged commit 13666b1 into staging May 9, 2026
14 checks passed
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8ca96c2. Configure here.

Comment thread scripts/sync-tool-catalog.ts
@waleedlatif1 waleedlatif1 deleted the fix/zustand-v5-compat branch May 9, 2026 23:48
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.

1 participant