Skip to content

Comments

refactor: replace toast bridge with RTK listener middleware#37

Merged
ryota-murakami merged 5 commits intomainfrom
feat/bulk-issues-20260220c
Feb 20, 2026
Merged

refactor: replace toast bridge with RTK listener middleware#37
ryota-murakami merged 5 commits intomainfrom
feat/bulk-issues-20260220c

Conversation

@ryota-murakami
Copy link
Contributor

@ryota-murakami ryota-murakami commented Feb 20, 2026

Summary

Architecture refactor of the Redux render-tracker/toaster pipeline, replacing the
timing-based hook bridge with RTK listener middleware for cleaner separation of concerns.

Changes per Issue

Issue #35: Bug fix

  • Add missing delete state.renderCountsByReason[name] in clearComponentHistory reducer

Issue #33: Listener middleware (main refactor)

  • Create src/store/listenerMiddleware.ts — intercepts recordRender actions, 300ms batching
  • Remove lastRender relay slot from renderTrackerSlice (resolves refactor: replace lastRender single-slot with event queue or middleware #36)
  • Delete useReRenderToasts hook (replaced by middleware)
  • Remove useReRenderToasts() call from AppShell in layout.tsx
  • Add listener middleware to store via .prepend()

Issue #34: Slice separation

  • Move suppressToasts, beginSuppressToasts, endSuppressToasts from renderTrackerSlice to toastSlice
  • renderTrackerSlice now has single responsibility: render data recording
  • Consumer imports unchanged (barrel export from @/store)

E2E fix

  • Scope hero section selectors in landing-page.spec.ts to avoid duplicate "React Docs" link match

Verification

  • Lint passing
  • TypeCheck passing
  • Build successful (20/20 static pages)
  • E2E: 7/7 render-tracking tests passing (toast behavior validated)
  • E2E: 5/5 landing-page tests passing

Summary by CodeRabbit

  • Refactor

    • Improved internal architecture and code organization for handling toast notifications. Existing toast behavior, batching, and suppression functionality remain unchanged.
  • Tests

    • Updated end-to-end test selectors for improved scoping.

Closes #35

clearComponentHistory cleared renderHistory and renderCounts but left
renderCountsByReason stale, creating a data inconsistency after reset.
Closes #33
Closes #36

- Create listenerMiddleware.ts that intercepts recordRender actions
  and dispatches toast notifications with 300ms batching
- Remove lastRender relay slot from renderTrackerSlice (resolves #36
  single-slot overwrite risk)
- Delete useReRenderToasts hook (no longer needed)
- Remove useReRenderToasts() call from AppShell in layout.tsx
- Add listener middleware to store via .prepend()

The suppressToasts/triple-setTimeout timing in useSuppressToasts is
retained — it's inherent to useRenderTracker's double-setTimeout
dispatch pattern and unrelated to the toast bridge mechanism.
Closes #34

Move suppressToasts state, beginSuppressToasts, and endSuppressToasts
from renderTrackerSlice to toastSlice. renderTrackerSlice now has a
single responsibility: recording render events (history, counts,
per-reason breakdowns). Toast suppression belongs to the toast domain.

Consumer code unchanged — all imports use @/store barrel export.
The footer added in PR #32 introduced a second "React Docs" link,
causing Playwright strict mode to fail on getByRole. Scope selectors
to the hero <header> element.
@vercel
Copy link

vercel bot commented Feb 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
re-render Ready Ready Preview, Comment Feb 20, 2026 0:45am

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

Warning

Rate limit exceeded

@ryota-murakami has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 22 minutes and 37 seconds before requesting another review.

⌛ 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.

📝 Walkthrough

Walkthrough

This PR replaces React hook-based toast signaling with Redux Toolkit listener middleware, removing the useReRenderToasts hook and its fragile triple-setTimeout pattern. Toast suppression state moves from renderTrackerSlice to toastSlice, and a new middleware handles render-to-toast conversion with 300ms debouncing. RenderTrackerSlice is simplified by removing lastRender and suppressToasts fields. E2E tests are refactored for improved selector scoping.

Changes

Cohort / File(s) Summary
Middleware Implementation
src/store/listenerMiddleware.ts
New Redux listener middleware that converts recordRender actions into toast notifications with 300ms debouncing. Buffers render events and dispatches single or batch toasts based on event count, respecting the suppressToasts flag.
Store Configuration & State
src/store/index.ts, src/store/toastSlice.ts, src/store/renderTrackerSlice.ts
Added listenerMiddleware to store. Moved suppressToasts and related reducers (beginSuppressToasts, endSuppressToasts) from renderTrackerSlice to toastSlice. Removed lastRender and suppressToasts fields from renderTrackerSlice. Fixed clearComponentHistory to cleanup renderCountsByReason entries.
Hook Removal
src/hooks/useReRenderToasts.ts, src/hooks/index.ts, src/app/layout.tsx
Deleted useReRenderToasts hook and removed its export. Removed hook invocation from AppShell in layout.tsx.
E2E Test Refactoring
e2e/tests/landing-page.spec.ts
Introduced hero variable for header scoping and updated all header-related selectors to use this scope for improved maintainability.

Sequence Diagram

sequenceDiagram
    participant Component
    participant Store as Redux Store
    participant Middleware as Listener Middleware
    participant Toast as Toast Slice

    rect rgba(100, 150, 200, 0.5)
    Note over Component,Toast: Old Flow (Hook-based)
    Component->>Store: dispatch recordRender()
    Store->>Component: Update lastRender state
    Component->>Component: useReRenderToasts reads lastRender<br/>(triple-setTimeout race)
    Component->>Toast: dispatch addToast/addBatchToast()
    end

    rect rgba(200, 150, 100, 0.5)
    Note over Component,Toast: New Flow (Middleware-based)
    Component->>Store: dispatch recordRender()
    Middleware->>Middleware: Buffer render event<br/>(300ms debounce)
    Middleware->>Middleware: On debounce timeout:<br/>flush buffer
    alt Single render event
        Middleware->>Toast: dispatch addToast()
    else Multiple render events
        Middleware->>Toast: dispatch addBatchToast()
    end
    Toast->>Toast: Update toast state
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Hops of joy! The timers are gone,

Middleware now captures each render at dawn,

No triple-setTimeout race to outrun,

Just debounced toasts—elegant and clean fun! 🎉

The hooks retire; state finds its true home. 🏠

🚥 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 PR title clearly and concisely summarizes the main architectural change: replacing the timing-based hook bridge (useReRenderToasts) with RTK listener middleware.
Linked Issues check ✅ Passed All four linked issues (#33, #34, #35, #36) are fully addressed: listener middleware replaces setTimeout [#33], toast signaling moved to toastSlice [#34], renderCountsByReason cleanup added [#35], and lastRender single-slot replaced by middleware event handling [#36].
Out of Scope Changes check ✅ Passed All changes are directly scoped to the four linked issues: store restructuring, hook removal, middleware addition, and E2E selector fix are all necessary for the refactoring objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/bulk-issues-20260220c

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.

Copy link

@coderabbitai coderabbitai bot left a comment

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)
src/store/listenerMiddleware.ts (1)

32-33: Module-level buffer/flushTimer state undermines testability (issue #33 acceptance criteria)

buffer and flushTimer live at module scope, meaning they are shared across all store instantiations within the same process. In tests that create multiple stores or that import this module without jest.resetModules() between runs, stale buffer contents or dangling timers from one test can bleed into the next — violating the "make toast logic testable and robust" requirement from issue #33.

The RTK-idiomatic debounce (cancelActiveListeners + listenerApi.delay()) doesn't directly support accumulate-all batching, so module-level state is the pragmatic choice here. At minimum, export a reset helper that tests can call in afterEach:

♻️ Suggested testability escape hatch
 /** Module-level buffer for batching render events within the debounce window */
 let buffer: RenderInfo[] = []
 let flushTimer: ReturnType<typeof setTimeout> | null = null

+/** Reset internal debounce state — use in test teardown (afterEach) only. */
+export const _resetBufferForTesting = () => {
+  buffer = []
+  if (flushTimer !== null) {
+    clearTimeout(flushTimer)
+    flushTimer = null
+  }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/store/listenerMiddleware.ts` around lines 32 - 33, Module-level shared
state buffer and flushTimer cause cross-test leakage; add and export a reset
helper (e.g. resetListenerMiddlewareState) that clears the buffer array
(buffer.length = 0) and cancels any pending timer (clearTimeout(flushTimer))
then sets flushTimer = null so tests can call it in afterEach; implement the
helper in the same module where buffer and flushTimer are declared and keep
types consistent (ReturnType<typeof setTimeout> | null) so callers can reliably
reset state between test runs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/store/listenerMiddleware.ts`:
- Around line 47-57: The flush callback for flushTimer currently dispatches
addToast/addBatchToast using the buffer captured at recordRender time without
re-checking getState().toast.suppressToasts, allowing a race where
suppressToasts can be set true after buffering but before flush; modify the
setTimeout callback in listenerMiddleware (the block that uses flushTimer,
buffer and BATCH_DEBOUNCE_MS) to call getState().toast.suppressToasts again at
the top of the callback and bail out (do not dispatch) if it is true, leaving
buffer cleared or handled as desired to prevent emitting toasts while
suppressed.

---

Nitpick comments:
In `@src/store/listenerMiddleware.ts`:
- Around line 32-33: Module-level shared state buffer and flushTimer cause
cross-test leakage; add and export a reset helper (e.g.
resetListenerMiddlewareState) that clears the buffer array (buffer.length = 0)
and cancels any pending timer (clearTimeout(flushTimer)) then sets flushTimer =
null so tests can call it in afterEach; implement the helper in the same module
where buffer and flushTimer are declared and keep types consistent
(ReturnType<typeof setTimeout> | null) so callers can reliably reset state
between test runs.

Addresses CodeRabbit review: suppressToasts was only checked at event
arrival, not at the 300ms flush. A UI chrome action between buffering
and flushing could bypass suppression. Now re-checks getState() at
flush time.
@ryota-murakami ryota-murakami merged commit becce40 into main Feb 20, 2026
3 checks passed
@ryota-murakami ryota-murakami deleted the feat/bulk-issues-20260220c branch February 20, 2026 12:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant