ui/goose2/ui_improvements/state_management/goose2-zustand-state-management-review.md
Goose2 Zustand Review: Code Quality + Architecture
If the bar is “easy to maintain later” and “low future bug surface,” the current Zustand usage in ui/goose2 does not meet that bar yet.
This review combines two perspectives:
The conclusions are based on reading the current ui/goose2 stores, major consumers, and store tests.
Primary reference points from the official Zustand docs:
useShallow: https://zustand.docs.pmnd.rs/learn/guides/prevent-rerenders-with-use-shallowpersist: https://zustand.docs.pmnd.rs/reference/middlewares/persistMap / Set usage: https://zustand.docs.pmnd.rs/learn/guides/maps-and-sets-usageOverall Verdict
ui/goose2 uses Zustand well enough to ship features, but not strictly enough for long-term maintainability.
The biggest problems are:
This is not a case of “Zustand is wrong.” It is a case of “the current architecture makes Zustand too easy to use as a global bucket for state, persistence, and orchestration.”
What Is Good
set and are implemented immutably.Map and Set usage is correct where present, because updates create new instances. Examples: providerInventoryStore.ts, chatStore.ts.getState() usage in non-React async/event code is acceptable and not a problem by itself.Detailed Findings
In React code, bound store hooks should not be called without selectors.
Concrete examples:
Why this is a problem:
Strict standard:
useSomeStore() without a selector in React components or hooksuseShallow is underused where it mattersThere is effectively no useShallow usage in the src/ tree.
That is not the primary problem, but it is a real gap after selector discipline is fixed.
Where it would matter:
Good target pattern:
const { sessions, activeSessionId } = useChatSessionStore(
useShallow((s) => ({
sessions: s.sessions,
activeSessionId: s.activeSessionId,
})),
);
Strict standard:
useShallow on object/array selectorsuseShallow on primitive selectorsMore specifically, useShallow is good for:
useShallow is not for:
loading, activeSessionId, or selectedProviderMaintainability conclusion:
useShallow is a good tactical improvement after selector cleanupSeparate stores are not automatically wrong, but several individual stores are carrying too many concerns.
Examples:
What that looks like:
chatStore mixes messages, runtime, drafts, queue, connection state, loading/replay state, scroll targeting, and cleanupagentStore mixes personas, agents, providers, selected provider, active agent, and persona editor UI statechatSessionStore mixes session records, active session selection, context-panel state, and workspace UI stateprojectStore mixes project records, loading state, persistence, CRUD orchestration, optimistic reordering, and active selectionWhy this is a problem:
Immer is relevant here only as an update-readability tool, not as a boundary fix. It may improve maintainability in nested update-heavy stores such as chatStore.ts, where updates to messagesBySession, sessionStateById, draftsBySession, and scrollTargetMessageBySession currently require substantial object spread boilerplate, and to a lesser extent in chatSessionStore.ts. It would not solve the higher-value problems in this review: broad store responsibilities, workflow/state mixing, hidden backend side effects, or ad hoc persistence. It is also unlikely to add much value to already-flat stores like providerInventoryStore.ts.
This is one of the clearest architectural problems.
Examples:
Specifically:
agentStore holds persona/agent/provider data and also persona editor modal statechatSessionStore holds session data and also context panel open state plus per-session workspace UI stateWhy this is a problem:
Strict standard:
The clearest example is chatSessionStore.updateSession.
Code:
Why this is a problem:
updateSession sounds like a local patch, but it may also rename a backend session or update the backend project associationStrict standard:
renameSessionAndPersistapi/ modules or dedicated hooks/commandsThis is the main architectural synthesis.
Examples:
What to look for:
Architectural conclusion:
api/ should own backend transportpersist would provide a cleaner boundaryExamples:
Why this is a problem:
localStorage codepartializeArchitectural conclusion:
The codebase often relies on imperative helper methods instead of a selector-oriented read layer.
Examples:
These helpers are acceptable for getState() usage in non-React code, but in React they push the app toward imperative reads instead of reactive selectors.
Architectural conclusion:
The clearest example is the sidebar.
Code:
Why this is a problem:
Architectural conclusion:
lib/ utilitiesprojectStore is the strongest example of architectural overreachCode:
It currently acts as:
Why this is a problem:
Examples:
What to look for:
Architectural conclusion:
Examples:
Why this is a problem:
setState shallow-mergesStrict standard:
getInitialState() patternWell-covered:
Noticeably under-covered:
Missing high-value coverage areas:
This is the meta architectural issue.
Code evidence comes from inconsistency across modules:
What this means:
Clarifications So We Don’t Overstate
Map and Set are not wrong here; they are used correctly.getState() is not wrong in non-React async/event code.Architectural Target Direction
The clean target architecture is:
api/ modules for backend transportlib/ utilities for derivation and transformationZustand should be a small, predictable shared-state layer, not a catch-all workflow layer.
A healthier direction would be to separate concerns more explicitly, for example:
This exact split does not need to be implemented all at once, but the codebase should move in that direction.
Recommended Standard For ui/goose2
useShallow only when a selector returns an object or array.persist for durable state, with partialize and version.getState() mainly in non-React async/event code.api/.Bottom Line
Strictly judged against Zustand good practices and from an architectural perspective, ui/goose2 is adequate for shipping features quickly, but not disciplined enough for long-term maintainability.
The highest-value issues to fix are:
These are the areas most likely to create future bugs and make the code harder to refactor, reason about, and test.