frontend: fix feature flag reactivity after MobX observer removal#2273
Conversation
LoginPage reads loginError through the legacy uiState proxy but is wrapped in observer() which only tracks MobX observables. After PR #2220 migrated loginError to Zustand, the component stopped re-rendering when loginError changed to null, leaving the modal permanently stuck open. Subscribe to loginError via useUIStateStore so the component properly re-renders when the error is dismissed.
Move endpointCompatibility into the Zustand supported-features store so feature flags recompute when backend data arrives. Components using useSupportedFeaturesStore selectors now re-render reactively, fixing ACL schema registry buttons staying disabled and shadow link card never appearing. Also silence console errors for SecretService and MCPServerService endpoints not yet returned by the backend.
0c7595f to
6151a6e
Compare
| const loginError = useUIStateStore((s) => s.loginError); | ||
| const setLoginError = useUIStateStore((s) => s.setLoginError); |
There was a problem hiding this comment.
I think we should check if there is a way to just do 1 useUIStateStore call and fetch both the getter and setter from it.
There was a problem hiding this comment.
had a dig into it and claude said the way its done is idiomatic zustand which pushed on it gave
const { loginError, setLoginError } = useUIStateStore(
useShallow((s) => ({
loginError: s.loginError,
setLoginError: s.setLoginError,
}))
dont know which is better
There was a problem hiding this comment.
ok I am fine either way, yes useShallow is to ensure there is no circular dependency.
|
@claude review |
|
Claude finished @c-julin's task in 2m 14s —— View job Code Review
Overall this is a well-structured fix. The core architecture — flattening feature flags into Zustand state and recomputing them atomically in
|
| Core fix | ✅ Correct approach, well-structured |
login.tsx |
useUIStateStore calls (minor, flagged by reviewer) |
Remaining isSupported() call-sites in components |
|
featureErrors replacement on recompute |
ℹ️ Subtle behavior, worth documenting |
| Test infra | ✅ Solid additions |
| return null; | ||
| } | ||
| this.endpointCompatibility = r.endpointCompatibility; | ||
| useSupportedFeaturesStore.getState().setEndpointCompatibility(r.endpointCompatibility); |
There was a problem hiding this comment.
not sure if we can call the hook in here since it might break the rules of React hooks - ideally the hook is called at all times and we just reference the state at the top.
There was a problem hiding this comment.
@claude check if this is ok according to react guidelines
There was a problem hiding this comment.
Claude finished @malinskibeniamin's task in 47s —— View job
- Read
backend-api.tsaround line 1018 - Analyze React hooks compliance
- Post findings
React hooks compliance at backend-api.ts:1018
This is fine. The concern is understandable, but line 1018 is not calling the React hook — it's calling Zustand's static .getState() method:
useSupportedFeaturesStore.getState().setEndpointCompatibility(r.endpointCompatibility);There's an important distinction here:
| Call | What it is | Rules of Hooks apply? |
|---|---|---|
useSupportedFeaturesStore() |
React hook | ✅ Yes — must be called at top level of a component/hook |
useSupportedFeaturesStore.getState() |
Static method on the store object | ❌ No — plain JS function call |
Zustand attaches getState() and setState() directly to the hook function object. These are ordinary JavaScript functions — they don't interact with React's fiber/hook system at all. The Zustand docs explicitly document this as the recommended pattern for reading/writing store state outside React components (utilities, API clients, async functions, etc.).
So backend-api.ts using .getState() here is the correct Zustand idiom and does not violate any React guidelines. No changes needed.
77165e1 to
bd1bd4f
Compare
Wait for connector deletion to propagate before recreating, and use Playwright's built-in retry helpers instead of manual retry loops.
bd1bd4f to
56f32ef
Compare
Summary
observer()removal.isSupported()was reading from MobXapi.endpointCompatibility, but components no longer re-rendered when that observable changed.Changes
endpointCompatibilityinto the ZustanduseSupportedFeaturesStoreas actual state, replacing JS getters with computed boolean properties that update whensetEndpointCompatibilityis calledbackend-api.tsnow syncs endpoint data to Zustand after fetchSecretServiceandMCPServerServiceto the list of services that gracefully returnfalsewhen absent from backend responseTest plan
bun run type:checkpassesbun run lintpasses (no new warnings)bun run test— 478/478 tests pass