Skip to content

useWidgetLayout's saveLayout writes to localStorage with no try/catch, unlike its own read path #1064

Description

@mikewheeleer

Description

src/components/treasuryOverviewPage/useWidgetLayout.ts's read path is carefully defensive:

const loadLayout = useCallback((): WidgetLayout => {
  const defaultLayout = generateDefaultLayout(metrics);
  try {
    const data = localStorage.getItem(storageKey);
    ...
  } catch (e) {
    console.warn("Failed to load layout from localStorage, falling back to default.", e);
    return defaultLayout;
  }
}, [metrics, storageKey, generateDefaultLayout]);

but its write path is not:

const saveLayout = useCallback((newLayout: WidgetLayout) => {
  setLayout(newLayout);
  localStorage.setItem(storageKey, JSON.stringify(newLayout));
}, [storageKey]);

localStorage.setItem throws (e.g. QuotaExceededError when storage is full, or a SecurityError in some private-browsing configurations) exactly like getItem can fail — but saveLayout has no try/catch around it. Since reorderWidgets, updateWidgetSize, updateWidgetVisibility, and resetLayout all funnel through saveLayout, any drag-and-drop reorder, resize, visibility toggle, or "Reset Layout" click (WidgetTray.tsx's "Reset Layout" button) performed while storage is full or unavailable will throw an uncaught exception from inside a React event handler, breaking the interaction the user just performed even though setLayout(newLayout) already optimistically updated the in-memory UI state one line earlier.

Requirements

  • saveLayout must not throw uncaught when localStorage.setItem fails; the in-memory layout state update should still succeed so the UI remains responsive even if persistence silently fails.
  • Failures should be logged (consistent with loadLayout's console.warn pattern) rather than swallowed silently or left to crash.

Suggested execution

  1. Wrap the localStorage.setItem call in saveLayout in a try/catch, logging a console.warn on failure (mirroring loadLayout's existing pattern) without re-throwing.
  2. Consider surfacing a subtle non-blocking indicator (or reuse ToastProvider) if persistence repeatedly fails, so users understand their widget layout preference isn't being saved — optional, but worth a TODO if out of scope for this fix.
  3. Add a test that mocks localStorage.setItem to throw and asserts reorderWidgets/updateWidgetSize still update in-memory layout state without throwing.

Acceptance criteria

  • saveLayout no longer throws uncaught when localStorage.setItem fails.
  • In-memory layout state still updates correctly even when persistence fails.
  • A regression test covers the storage-failure path.

Security notes

None; resilience/correctness fix.

Guidelines

  • Minimum 95% test coverage
  • Timeframe: 96 hours

Metadata

Metadata

Assignees

No one assigned

    Labels

    GRANTFOX OSSGrantFox open-source campaign taskMAYBE REWARDEDMay be rewarded under the GrantFox campaignOfficial Campaign | FWC26GrantFox FWC26 official campaignbugSomething isn't workingfrontend

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions