Skip to content

The settings theme effect leaks its matchMedia listener: cleanup removes a different handler, so the listener lives forever #396

Description

@usmanimamu17-create

Problem

src/app/setting/page.tsx adds a system-preference listener and cleans it up with a different function:

const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', () => {
  if (theme === 'system') { applyTheme('system'); }
});

return () => mediaQuery.removeEventListener('change', () => {});

removeEventListener requires the same function reference; the cleanup registers a new anonymous function, so the original change listener is never removed. Consequences:

  • The listener leaks on every theme change: each theme state change re-runs the effect, adding another change listener that is never removed — the leak grows with every theme toggle, and each leaked listener closes over the old theme value (stale closure: after switching to dark, the leaked system-era listeners still check the old theme).
  • Unmount never cleans up: navigating away from settings leaves the listener attached to window.matchMedia for the page's lifetime.
  • The cleanup pattern is a trap: the () => {} placeholder signals the author knew cleanup was required but wired the wrong reference — exactly the class of bug a lint rule (react-hooks/exhaustive-deps won't catch it) or a test should.

Root cause

The cleanup was written with a placeholder handler instead of the registered one.

Why this is architecturally hard

  1. The fix is to hold the handler in a variable and pass the same reference to both add and remove — but the handler must read the current theme (a ref, not the closure), or the listener should be re-created per theme change with correct cleanup.
  2. The theme logic is inline in the page; a useTheme hook (with the media-query handling and localStorage persistence) would centralize it and make the lifecycle testable — the durable fix is extraction, not just the reference fix.
  3. Testing listener leaks requires mocking matchMedia and asserting add/remove call counts — a test harness the page currently lacks.

Proposed design

Extract the theme handling into a useTheme hook (or fix the reference and use a themeRef for the handler), ensuring add/remove use the same reference and unmount removes the listener. Add a test asserting one listener per mount and zero after unmount.

Acceptance criteria

Service

  • The system-preference listener is removed on unmount and on theme change cleanup.
  • Theme behavior (system/dark/light) is unchanged.

Tests

  • A test mocks matchMedia and asserts balanced add/remove.
  • Existing settings tests pass.

Out of scope

The locale/theme preference sync to the server (companion issues).

Getting started

npm test

Good first files to read: src/app/setting/page.tsx.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardStellar Waveissue-trackingThird CampaignCampaign: Third Campaignarea/uiImported campaign issue labelpriority/mediumImported campaign issue label

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions