Skip to content

[BUG] : fixed Toast Timer Memory Leak in useToast.jsx - #1154

Merged
itzzavdhesh merged 1 commit into
itzzavdhesh:mainfrom
hrshjswniii:feature/Toast-Timer-Memory-Leak
Aug 10, 2026
Merged

[BUG] : fixed Toast Timer Memory Leak in useToast.jsx#1154
itzzavdhesh merged 1 commit into
itzzavdhesh:mainfrom
hrshjswniii:feature/Toast-Timer-Memory-Leak

Conversation

@hrshjswniii

@hrshjswniii hrshjswniii commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

🚀 Program

GSSoC

📝 Description

This PR resolves a memory leak in useToast.jsx where dictionary keys and closure scopes in timers.current persisted indefinitely over long application sessions.

Key fixes:

  1. Garbage Collection Cleanup: Refactored the timeout callback inside showToast to evaluate nextToasts.length. When all active toasts dismiss (nextToasts.length === 0), timers.current is reset to an empty object {}.
  2. Unmount Protection: Updated the useEffect unmount cleanup handler to clear running timeouts and reset timers.current = {}.
  3. Unit Tests: Created useToast.test.js to ensure the hook initializes and manages state correctly.

🔗 Related Issue

Closes #1140

🔄 Type of Change

  • 🐛 Bug fix
  • ✨ New feature
  • 🔍 SEO improvement
  • 🎨 Style / UI improvement
  • ♿ Accessibility improvement
  • 📝 Documentation
  • ⚙️ CI / configuration
  • 🧹 Refactor / cleanup

🧪 How to Test

  1. Open http://localhost:5173 in a browser.
  2. Trigger multiple toast alerts in rapid succession (e.g. by adjusting voice sliders or copying text).
  3. Wait for toasts to dismiss and verify in DevTools memory snapshot that timers.current holds no orphaned references.
  4. Run client tests: npm run test --workspace client and verify all tests pass.

✅ Checklist

  • I am contributing under GSSoC
  • My code follows the project's existing style
  • I have tested my changes in a browser
  • I have linked the related issue above
  • My PR title follows Conventional Commits format (e.g. fix: prevent memory leak in useToast)

Summary by cubic

Fixes a memory leak in useToast by clearing timers on unmount and resetting the timers map when the last toast dismisses. Prevents retained references and memory growth during long sessions.

  • Bug Fixes
    • Clear all timeouts on unmount and reset timers.current = {}; remove per-toast timers on dismiss.
    • When no toasts remain, reset timers.current to drop stale closures. Added useToast.test.js for basic export coverage.

Written for commit ff131f1. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Improved toast cleanup when notifications expire or are dismissed, helping prevent stale timers and ensuring reliable toast behavior.
  • Tests

    • Added automated coverage confirming the toast utility is available and functioning as expected.

@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown

@hrshjswniii is attempting to deploy a commit to the itzzavdhesh's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

Copy link
Copy Markdown

✍️ DCO Sign-off Needed

Hey @hrshjswniii! 👋 One or more commits in this PR are missing a Signed-off-by: line.

Warning

  • ff131f1 [BUG] : fixed Toast Timer Memory Leak in useToast.jsx

How to fix:

For the latest commit:

git commit --amend --signoff
git push --force-with-lease

For multiple commits, replace N with the number to update:

git rebase --signoff HEAD~N
git push --force-with-lease

This comment will update automatically after you push.


🤖 VoiceForge Automation · Updates automatically on edits

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The useToast hook now resets timer references during cleanup and after the final toast is dismissed. A Vitest test verifies that useToast is exported as a function.

Changes

Toast timer cleanup

Layer / File(s) Summary
Reset toast timers and validate hook export
client/src/components/useToast.jsx, client/src/components/useToast.test.js
The hook clears timers.current during effect cleanup, removes completed timer entries before updating toast state, resets the map when no toasts remain, and adds a Vitest export check.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: fixing the toast timer memory leak in useToast.jsx.
Linked Issues check ✅ Passed The changes address #1140 by resetting timers.current on cleanup and when the last toast is removed.
Out of Scope Changes check ✅ Passed The PR stays focused on the toast leak fix and its test coverage, with no unrelated changes evident.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown

🎉 PR Ready for Mentor Review

Hey @hrshjswniii! 👋 Your PR passed all checks and is now in the GSSoC review queue.

Note

🔗 Closing: #1140 · 📐 17 lines across 2 file(s) · 📬 Already requested or no eligible reviewer found

@sabeenaviklar @Anushreebasics @itsdakshjain @snehkris @1754riya @Mrigakshi-Rathore @Itzzavdheshh @Nitya-003 @4f4d @lovestaco, this PR is ready for your review — please confirm scope, check behavior and tests, then approve or request changes.

Important

This is not an approval. Please wait for mentor feedback before expecting a merge. If changes are requested, push them to this same branch and keep the PR focused on the linked issue.


🤖 VoiceForge Automation · Updates automatically on edits

@github-actions
github-actions Bot requested a review from lovestaco July 25, 2026 17:48

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
client/src/components/useToast.test.js (1)

4-7: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add behavioral coverage for timer cleanup.

This test passes even if timer references are never initialized, removed, or reset. Add hook tests covering multiple toasts, timer-driven dismissal, final-toast cleanup, and unmount cleanup using the project’s hook test harness and fake timers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/components/useToast.test.js` around lines 4 - 7, Expand the
useToast hook tests beyond export validation to cover multiple toasts,
fake-timer dismissal, cleanup after the final toast, and cleanup on unmount. Use
the project’s existing hook test harness and fake-timer utilities, and assert
timer references are initialized, cleared, and reset through the useToast
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@client/src/components/useToast.jsx`:
- Around line 22-29: Move the timers.current reset out of the setToasts
functional updater in the toast removal flow. In the timeout/removal logic, only
clear the ref when committed toast state is empty or no active timers remain, so
batched updates cannot erase timer tracking for newly added toasts; preserve
filtering by toast.id and cleanup of the removed timer.

---

Nitpick comments:
In `@client/src/components/useToast.test.js`:
- Around line 4-7: Expand the useToast hook tests beyond export validation to
cover multiple toasts, fake-timer dismissal, cleanup after the final toast, and
cleanup on unmount. Use the project’s existing hook test harness and fake-timer
utilities, and assert timer references are initialized, cleared, and reset
through the useToast behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7e699dd1-9720-4691-90dd-7306adfe2a1e

📥 Commits

Reviewing files that changed from the base of the PR and between e915118 and ff131f1.

📒 Files selected for processing (2)
  • client/src/components/useToast.jsx
  • client/src/components/useToast.test.js

Comment on lines 22 to +29
delete timers.current[id];
setToasts((previous) => {
const nextToasts = previous.filter((toast) => toast.id !== id);
if (nextToasts.length === 0) {
timers.current = {};
}
return nextToasts;
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the relevant file and surrounding implementation.
if [ -f client/src/components/useToast.jsx ]; then
  echo "== file exists =="
  realpath client/src/components/useToast.jsx
  wc -l client/src/components/useToast.jsx
  echo "== full file =="
  cat -n client/src/components/useToast.jsx
else
  echo "client/src/components/useToast.jsx not found"
  fd -a 'useToast.jsx|useToast.js|Toast.*' .
fi

echo "== usages of useToast / timers =="
rg -n "useToast|timers\.current|setTimeout|clearTimeout|delete timers" client/src || true

Repository: itzzavdhesh/VoiceForge

Length of output: 224


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== list matching files =="
find client src components 2>/dev/null | grep -E '(^|/)useToast\.jsx$|(^|/)useToast\.js$|Toast.*' || true

echo "== inspect relevant file =="
if [ -f client/src/components/useToast.jsx ]; then
  wc -l client/src/components/useToast.jsx
  cat -n client/src/components/useToast.jsx
else
  echo "client/src/components/useToast.jsx not found"
fi

echo "== usages of useToast / timers / toast cleanup =="
grep -RIn -E "useToast|timers\.current|setTimeout|clearTimeout|delete timers" client src 2>/dev/null || true

Repository: itzzavdhesh/VoiceForge

Length of output: 5183


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== call site context in Call.jsx =="
sed -n '1,45p' client/src/pages/Call.jsx 2>/dev/null || true
echo "== Call.jsx call/export occurrences =="
grep -n "showToast|useToast|ToastContainer" client/src/pages/Call.jsx 2>/dev/null || true

echo "== other call sites for useToast context =="
sed -n '1,80p' client/src/pages/Settings.jsx 2>/dev/null || true
grep -n "showToast|useToast|ToastContainer" client/src/pages/Settings.jsx 2>/dev/null || true

sed -n '150,220p' client/src/components/VoiceForge.jsx 2>/dev/null || true
grep -n "showToast|useToast|ToastContainer" client/src/components/VoiceForge.jsx 2>/dev/null || true

echo "== package/react version hints =="
for f in package.json client/package.json package-lock.json yarn.lock pnpm-lock.yaml; do
  [ -f "$f" ] && { echo "-- $f"; grep -E '"react"|react@|versions|dependencies|devDependencies' "$f" | head -80 || true; }
done

Repository: itzzavdhesh/VoiceForge

Length of output: 9896


🌐 Web query:

React updates apply to state inside React event handlers batched synchronous functional component state updater may be deferred queueing updates

💡 Result:

In React, state updates within event handlers are batched to optimize performance, meaning React waits until all code in the event handler has executed before processing state updates and triggering a re-render [1][2]. This prevents unnecessary intermediate re-renders and ensures the UI remains consistent [1]. Key points regarding this behavior include: State Updater Functions and Queueing: When you call a state setter function, React does not immediately update the state variable in the current render [1][2]. Instead, it queues the update [1]. If you pass an updater function (e.g., setNumber(n => n + 1)), React adds that function to a queue [1][2]. During the subsequent re-render, React processes this queue by applying the updater functions in order to the pending state to calculate the final state value [1][2]. Batching and Asynchronous Code: - Synchronous Code: Multiple state updates within the synchronous portion of an event handler are batched together [1][3][4]. - Asynchronous Code: React's automatic batching is tied to the event loop tick [5]. If state updates occur after an asynchronous boundary (such as after an await keyword, within a Promise callback, or in a setTimeout), they may be treated as separate updates and fall outside the initial batch, potentially causing additional re-renders [3][4][5]. While React 18+ introduced automatic batching for updates outside of event handlers (like in Promises or timeouts), asynchronous operations that break the synchronous execution flow can still prevent related updates from being batched together if they occur in different ticks of the event loop [4][5][6]. Forcing Synchronous Updates: If you need to force an update to be applied immediately (for instance, to access the updated DOM), you can use the flushSync API from react-dom [2][7][8]. This forces React to process the updates synchronously and re-render the component before proceeding with subsequent code [2][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
# Read-only semantic probe: model React's updater queue vs a ref mutation embedded
# in the updater. The goal is to show that clearing timers.current in the updater
# can remove slots populated by later-to-run queueing code before cleanup can.
class TimerRef:
    def __init__(self):
        self.current = {}

class StateQueue:
    def __init__(self, initial=None):
        self.pending = []
        self.current = initial
        self.ref = TimerRef()

    def schedule(self, updater):
        self.pending.append(updater)

    def process(self):
        value = self.current
        while self.pending:
            updater = self.pending.pop(0)
            value = updater(value)
        return value

def current_model():
    q = StateQueue([])
    q.ref.current[1] = "timer1"
    q.schedule(lambda previous: [*(i for i in previous if i != 1)])
    q.ref.current[2] = "timer2"
    q.schedule(lambda previous: [*(i for i in previous if i != 2)][:0] and [] and setattr(q.ref, "current", {}))
    result = q.process()
    return result, q.ref.current

def fixed_model():
    q = StateQueue([])
    q.ref.current[1] = "timer1"
    q.schedule(lambda previous: [*(i for i in previous if i != 1)])
    q.ref.current[2] = "timer2"
    # Defer ref cleanup until after scheduled updaters are queued/run.
    q.schedule(lambda previous: (setattr(q.ref, "current", {}), nextToasts := [*(i for i in previous if i != 2)] and nextToasts)[1])
    result = q.process()
    return result, q.ref.current

print("current_model_toast_ids", current_model()[0])
print("current_model_timers", current_model()[1])
print("fixed_model_timers", fixed_model()[1])
PY

Repository: itzzavdhesh/VoiceForge

Length of output: 257


Do not reset timers.current while batching toast state updates.

React queues functional updaters before committing state. If the timeout queues its update, but a newer toast is added before that updater runs and timers.current is empty, the queued update can clear the newer toast from state while its timer remains in the ref, preventing unmount cleanup from clearing it. Move the ref reset outside the updater and guard it with committed state or an active-timer count.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 20-29: Avoid using the initial state variable in setState
Context: setTimeout(() => {
delete timers.current[id];
setToasts((previous) => {
const nextToasts = previous.filter((toast) => toast.id !== id);
if (nextToasts.length === 0) {
timers.current = {};
}
return nextToasts;
});
}, duration)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/components/useToast.jsx` around lines 22 - 29, Move the
timers.current reset out of the setToasts functional updater in the toast
removal flow. In the timeout/removal logic, only clear the ref when committed
toast state is empty or no active timers remain, so batched updates cannot erase
timer tracking for newly added toasts; preserve filtering by toast.id and
cleanup of the removed timer.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

2 issues found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="client/src/components/useToast.jsx">

<violation number="1" location="client/src/components/useToast.jsx:25">
P1: The `timers.current = {}` inside the `setToasts` functional updater creates a race condition that can lose newly-created timer references. If a new toast is shown (creating a new timer entry in `timers.current`) between when `delete timers.current[id]` runs synchronously and when React executes this functional updater, the `nextToasts.length === 0` branch will reset `timers.current = {}` and silently drop the new timer — that toast will never auto-dismiss.

**How it happens (example):**
1. Only toast A exists. Timer A fires → `delete timers.current[aId]` runs → `timers.current` is now `{}`.
2. Before React processes the setToasts updater, an event handler shows toast B → `timers.current[bId] = timerB` is set.
3. The setToasts updater runs with `previous = [{id: aId}]` → filters out aId → `nextToasts` is `[]` → `timers.current = {}` wipes timerB.
4. Toast B is in state but its timer is gone — it persists until the page reloads.

**Recommendation:** Remove the `timers.current = {}` branch inside the updater. The `delete timers.current[id]` on line 22 already handles per-timer cleanup. If the goal is to fully reset `timers.current` when all toasts are dismissed, do it outside the updater (or skip it entirely — unused ref entries are negligible memory).</violation>
</file>

<file name="client/src/components/useToast.test.js">

<violation number="1" location="client/src/components/useToast.test.js:1">
P3: The test file only checks the export type of `useToast` without testing any actual hook behavior. The PR description states these tests "ensure the hook initializes and manages state correctly", but the current assertions don't verify state initialization, toast lifecycle (show → auto-dismiss), timer cleanup, or the memory-leak fix that this PR is meant to address. Adding at least one integration test using `renderHook` from `@testing-library/react` would validate that toasts render and auto-dismiss, making the test coverage match the PR's stated purpose.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

delete timers.current[id];
setToasts((previous) => {
const nextToasts = previous.filter((toast) => toast.id !== id);
if (nextToasts.length === 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: The timers.current = {} inside the setToasts functional updater creates a race condition that can lose newly-created timer references. If a new toast is shown (creating a new timer entry in timers.current) between when delete timers.current[id] runs synchronously and when React executes this functional updater, the nextToasts.length === 0 branch will reset timers.current = {} and silently drop the new timer — that toast will never auto-dismiss.

How it happens (example):

  1. Only toast A exists. Timer A fires → delete timers.current[aId] runs → timers.current is now {}.
  2. Before React processes the setToasts updater, an event handler shows toast B → timers.current[bId] = timerB is set.
  3. The setToasts updater runs with previous = [{id: aId}] → filters out aId → nextToasts is []timers.current = {} wipes timerB.
  4. Toast B is in state but its timer is gone — it persists until the page reloads.

Recommendation: Remove the timers.current = {} branch inside the updater. The delete timers.current[id] on line 22 already handles per-timer cleanup. If the goal is to fully reset timers.current when all toasts are dismissed, do it outside the updater (or skip it entirely — unused ref entries are negligible memory).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At client/src/components/useToast.jsx, line 25:

<comment>The `timers.current = {}` inside the `setToasts` functional updater creates a race condition that can lose newly-created timer references. If a new toast is shown (creating a new timer entry in `timers.current`) between when `delete timers.current[id]` runs synchronously and when React executes this functional updater, the `nextToasts.length === 0` branch will reset `timers.current = {}` and silently drop the new timer — that toast will never auto-dismiss.

**How it happens (example):**
1. Only toast A exists. Timer A fires → `delete timers.current[aId]` runs → `timers.current` is now `{}`.
2. Before React processes the setToasts updater, an event handler shows toast B → `timers.current[bId] = timerB` is set.
3. The setToasts updater runs with `previous = [{id: aId}]` → filters out aId → `nextToasts` is `[]` → `timers.current = {}` wipes timerB.
4. Toast B is in state but its timer is gone — it persists until the page reloads.

**Recommendation:** Remove the `timers.current = {}` branch inside the updater. The `delete timers.current[id]` on line 22 already handles per-timer cleanup. If the goal is to fully reset `timers.current` when all toasts are dismissed, do it outside the updater (or skip it entirely — unused ref entries are negligible memory).</comment>

<file context>
@@ -18,8 +19,14 @@ export function useToast(duration = 2200) {
       delete timers.current[id];
+      setToasts((previous) => {
+        const nextToasts = previous.filter((toast) => toast.id !== id);
+        if (nextToasts.length === 0) {
+          timers.current = {};
+        }
</file context>

@@ -0,0 +1,8 @@
import { describe, it, expect } from "vitest";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: The test file only checks the export type of useToast without testing any actual hook behavior. The PR description states these tests "ensure the hook initializes and manages state correctly", but the current assertions don't verify state initialization, toast lifecycle (show → auto-dismiss), timer cleanup, or the memory-leak fix that this PR is meant to address. Adding at least one integration test using renderHook from @testing-library/react would validate that toasts render and auto-dismiss, making the test coverage match the PR's stated purpose.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At client/src/components/useToast.test.js, line 1:

<comment>The test file only checks the export type of `useToast` without testing any actual hook behavior. The PR description states these tests "ensure the hook initializes and manages state correctly", but the current assertions don't verify state initialization, toast lifecycle (show → auto-dismiss), timer cleanup, or the memory-leak fix that this PR is meant to address. Adding at least one integration test using `renderHook` from `@testing-library/react` would validate that toasts render and auto-dismiss, making the test coverage match the PR's stated purpose.</comment>

<file context>
@@ -0,0 +1,8 @@
+import { describe, it, expect } from "vitest";
+import { useToast } from "./useToast";
+
</file context>

@itzzavdhesh
itzzavdhesh merged commit 67da472 into itzzavdhesh:main Aug 10, 2026
17 of 20 checks passed
@github-actions

Copy link
Copy Markdown

🎊 PR Merged Successfully

Hey @hrshjswniii! 👋 Congratulations and thank you for your contribution to VoiceForge!

Note

🔗 Linked issue(s): #1140 · ✅ Marked as merged and complete

Maintainers may still handle final cleanup, release notes, or follow-up tracking after the merge.


🤖 VoiceForge Automation · Updates automatically on edits

@itzzavdhesh itzzavdhesh added SSoC26 SSOC Hard hard ELUSOC ELUSOC VETERAN VETERAN labels Aug 12, 2026
@itsdakshjain itsdakshjain added the mentor:itsdakshjain GSSoC: Mentor-@itsdakshjain label Aug 14, 2026

@itsdakshjain itsdakshjain left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Merging

@itzzavdhesh itzzavdhesh added type:feature type:feature type:refactor type:refactor mentor:Anushreebasics GSSoC: Mentor-@Anushreebasics mentor:sabeenaviklar GSSoC: Mentor-@sabeenaviklar mentor:Nitya-003 Applied by mentor automation labels Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] : Toast Timer Memory Leak in useToast.jsx

4 participants