Skip to content

fix(ui-kit): StateBoundary re-notifies on every re-render while isError is true, not once per transition #7436

Description

@JSONbored

Context

packages/loopover-ui-kit/src/components/state-views.tsx's StateBoundary component has a
useEffect (lines 267–280) whose own leading comment states the intent explicitly:

// When this boundary flips into the error state, surface a toast with Retry.
useEffect(() => {
  if (isError && errorLabel) {
    onFailureNotify?.({ label: errorLabel, kind: errorKind ?? "network", message: ..., retry: onRetry });
  }
}, [isError, errorLabel, errorKind, resolvedErrorDescription, onRetry, onFailureNotify]);

"Flips into the error state" describes an edge-triggered notification — fire once, on the
false→true transition. The implementation is level-triggered: it has no memory of the previous
isError value, so it re-fires on any render where isError is true and any of the six listed
deps changed identity — including onRetry and onFailureNotify, which are ordinary function props.

Every real call site goes through the app-level wrapper at
apps/loopover-ui/src/components/site/state-views.tsx:

export function StateBoundary(props: ComponentProps<typeof UiKitStateBoundary>) {
  return <UiKitStateBoundary onFailureNotify={(args) => notifyApiFailure(args)} {...props} />;
}

This wrapper is itself a plain function component re-invoked on every parent render, and it
constructs a brand-new arrow function for onFailureNotify each time (deliberately, per its own
comment, to keep notifyApiFailure lazily-referenced) — so onFailureNotify's identity is
guaranteed to change on every single render of any component using errorLabel, regardless of
whether the underlying error state actually changed. Six call sites currently pass errorLabel
(owner-panel.tsx, digest-panel.tsx, commands-panel.tsx, app.operator.tsx, app.runs.tsx,
app.analytics.tsx) — any unrelated re-render of these panels while an error is showing (a sibling
state update, a timer tick, an unrelated prop change) re-invokes notifyApiFailure.

The correct, edge-triggered pattern already exists elsewhere in this same codebase:
apps/loopover-ui/src/lib/mcp-version-badge.tsx (its wasError ref) tracks the previous error
value explicitly and only calls notifyApiFailure/notifyApiRecovered on an actual transition. That
is the precedent to mirror.

notifyApiFailure (apps/loopover-ui/src/lib/api/request.ts) does have its own dedupe-by-id and a
repeat counter, so this does not spam stacked toasts — but it does mean the toast content churns
("still failing (Nx in a row)") and notifyApiFailure executes far more often than the "flips into"
comment describes, on every affected re-render rather than on the actual state transition. No
existing test (apps/loopover-ui/src/components/site/state-views.test.tsx) asserts a call count
across re-renders — every existing assertion only checks that notifyApiFailure was called with the
right arguments at least once, so this gap has no regression coverage today.

Requirements

  • In packages/loopover-ui-kit/src/components/state-views.tsx's StateBoundary, change the
    onFailureNotify effect to be genuinely edge-triggered: track whether the boundary was already in
    the error state on the previous render (a useRef<boolean>, mirroring the wasError pattern in
    apps/loopover-ui/src/lib/mcp-version-badge.tsx) and only call onFailureNotify on the
    false→true transition, not on every render where isError is already true.
  • The transition must still correctly re-fire if the boundary leaves and re-enters the error state
    (e.g. error → ready → error again) — this is a "false→true edge" fix, not a "call at most once
    ever" fix.
  • apps/loopover-ui/src/components/site/state-views.tsx's wrapper's inline
    onFailureNotify={(args) => notifyApiFailure(args)} may stay exactly as-is once the ui-kit side is
    edge-triggered — do not "fix" it by trying to memoize the wrapper's arrow function instead;
    the fix belongs in the boundary's own edge-detection, not in papering over one unstable caller.
  • Do not change onRetry's own click-triggered call path (the Retry button dispatch) — only the
    passive onFailureNotify effect is in scope.

Deliverables

  • StateBoundary in packages/loopover-ui-kit/src/components/state-views.tsx only invokes
    onFailureNotify on a real false → true transition of isError (with errorLabel present),
    never on a same-state re-render.
  • A regression test added to apps/loopover-ui/src/components/site/state-views.test.tsx that:
    renders StateBoundary with isError errorLabel="X", re-renders it several times with isError
    still true but a different onRetry/onFailureNotify identity each time (simulating the real
    wrapper), and asserts the notify callback was invoked exactly once — then transitions
    isError to false and back to true and asserts it fires again exactly once for the second
    transition.

Test Coverage Requirements

packages/loopover-ui-kit is not part of vitest.config.ts's coverage.include list and is not
measured by Codecov's patch gate — the acceptance signal for a PR here is a green local test run, not
a Codecov percentage. The regression test itself is still mandatory per this repo's own testing
discipline (a fix without a regression test is incomplete), it just isn't Codecov-gated. If this fix's
test happens to be added to apps/loopover-ui/src/components/site/state-views.test.tsx instead (also
apps/**, also outside coverage.include), the same applies there.

Expected Outcome

A component using errorLabel that stays in the error state across several unrelated re-renders
triggers exactly one failure notification per actual error-state entry, matching the effect's own
"flips into the error state" documented intent — not one notification per re-render.

Links & Resources

  • packages/loopover-ui-kit/src/components/state-views.tsx:267-280
  • apps/loopover-ui/src/components/site/state-views.tsx:23-25
  • apps/loopover-ui/src/lib/mcp-version-badge.tsx:17-33 (the edge-triggered pattern to mirror)
  • apps/loopover-ui/src/components/site/state-views.test.tsx (where to add the regression test)

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.help wantedExtra attention is needed

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions