Skip to content

fix(toast): memoize useToast factory to neutralize storm loop#601

Merged
Blb3D merged 1 commit into
mainfrom
fix/toast-storm-usememo
May 11, 2026
Merged

fix(toast): memoize useToast factory to neutralize storm loop#601
Blb3D merged 1 commit into
mainfrom
fix/toast-storm-usememo

Conversation

@Blb3D
Copy link
Copy Markdown
Owner

@Blb3D Blb3D commented May 11, 2026

Summary

Wraps the toast factory object in ToastProvider with useMemo([addToast]) so the context value is stable across renders. Without it, every render of ToastProvider (which happens on every setToasts(...) call) produces a new object literal, breaking referential equality for every consumer that depends on toast.

Closes cortex_observe #70 (filed against AdminAccessRequests.jsx during the 2026-05-10 incident).

The loop this prevents

AdminAccessRequests.jsx:31-51:

```jsx
const fetchRequests = useCallback(async () => {
try {
const data = await api.get('/api/v1/pro/portal/admin/access-requests');
setRequests(data);
} catch (err) {
toast.error('Failed to load access requests');
} finally { setLoading(false); }
}, [api, filter, toast]);

useEffect(() => {
if (isPro && !flagsLoading) fetchRequests();
}, [isPro, flagsLoading, fetchRequests]);
```

If the fetch fails (e.g. PRO routes returning 403 `no_license` during a schema drift like Aeonyx #148), the chain is:

  1. fetch fails → `toast.error(...)`
  2. `addToast` → `setToasts(...)` → `ToastProvider` re-renders
  3. New `toast` object literal → context value changes
  4. `AdminAccessRequests` re-renders → `useToast()` returns new ref
  5. `fetchRequests` useCallback rebuilds (dep `toast` changed) → new ref
  6. `useEffect` re-fires (dep `fetchRequests` changed)
  7. Loop back to step 1

Visible as a pile-up of identical toasts plus continuous request load.

Independence from PR #600

This is an independent defensive fix. PR #600 fixes the underlying `license_gate` 403 chain that triggered the loop on the BLB3D VM. This PR ensures the same class of loop can't be triggered by any future 4xx on any admin page that calls `toast.error` inside a useEffect. They can land in either order.

Why useMemo is correct here

`addToast` is wrapped in `useCallback([], ...)` — its identity is stable across the lifetime of the provider. The memo cache therefore never invalidates (the dep array of `[addToast]` is effectively a no-op dep array), and the four method references inside the factory remain stable too.

Test plan

  • Single one-file change, no behavioral surface change beyond ref stability
  • Existing `useToast` consumers continue to work — the API surface (`.success/.error/.warning/.info`) is identical
  • After both this PR and PR feat(pr-03): extend LicenseCache to match filaops-pro reader contract #600 merge: load /admin/access-requests; verify single error toast on first 403, no pile-up

Out of scope

🤖 Generated with Claude Code
Agent-Session: 0f282673-19c5-4c14-b7ad-b397d94cc586

The ToastProvider value was an object literal recreated on every render.
Any consumer that included `toast` in a useCallback or useEffect dep
array would rebuild on every render of the provider, which renders
every time addToast updates `toasts` state. If that consumer's effect
called toast.error on failure (e.g. AdminAccessRequests.jsx loading
/api/v1/pro/portal/admin/access-requests, which 403s when
PRO is unlicensed), the loop self-amplifies: error → toast.error →
provider re-render → new toast ref → useCallback rebuilds →
useEffect re-fires → error → ...

Wrapping the factory in useMemo([addToast]) makes the context value
stable across re-renders. addToast itself is stable (useCallback with
[]), so the memo cache effectively pins the toast object for the
lifetime of the provider.

Independent fix; the underlying 403-on-every-PRO-route trigger is
addressed separately in PR #600 (license cache schema PR-03).

Closes cortex_observe #70.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 11, 2026 14:01
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 2026

Warning

Rate limit exceeded

@Blb3D has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 58 minutes and 45 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 41a27915-7c81-43dc-8458-0f34bc6cca59

📥 Commits

Reviewing files that changed from the base of the PR and between d7326ac and a316380.

📒 Files selected for processing (1)
  • frontend/src/components/Toast.jsx
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/toast-storm-usememo

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 11, 2026

Review Council Results

0 tests   0 ✅  0s ⏱️
0 suites  0 💤
0 files    0 ❌

Results for commit a316380.

♻️ This comment has been updated with latest results.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Stabilizes the ToastProvider context value by memoizing the toast factory object so consumers can safely include toast in hook dependency arrays without triggering effect/callback rebuild loops on every toast render.

Changes:

  • Add useMemo to Toast.jsx imports.
  • Wrap the toast object literal in useMemo([addToast]) to preserve referential equality across provider re-renders.
  • Document the rationale (prevents “toast storm” loop when consumers call toast.error inside effects).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@Blb3D Blb3D merged commit 9386f51 into main May 11, 2026
24 of 27 checks passed
@Blb3D Blb3D deleted the fix/toast-storm-usememo branch May 11, 2026 14:22
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.

2 participants