Skip to content

Commit 7a432c8

Browse files
authored
Merge pull request #439 from Weaverse/fix/431-theme-settings-store-max-listeners
2 parents 2a28a7d + a527cd9 commit 7a432c8

11 files changed

Lines changed: 920 additions & 12 deletions
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Fix ThemeSettingsStore Max Listeners Exceeded (Issue #431)
2+
3+
**Date:** 2025-07-15
4+
**Status:** Approved
5+
**Scope:** SDK fix + Pilot template optimization + tests
6+
7+
## Problem
8+
9+
Console warning appears on all Weaverse Hydrogen themes:
10+
```
11+
ThemeSettingsStore: Maximum listeners (100) exceeded. Possible memory leak detected.
12+
```
13+
14+
The `ThemeSettingsStore` class in `packages/hydrogen/src/utils/use-theme-settings-store.ts` has a hard cap of 100 listeners. When exceeded, new subscriptions are silently dropped (returns a no-op cleanup function), causing components to stop receiving theme setting updates.
15+
16+
### Why 100 Is Too Low
17+
18+
In the Pilot template, `useThemeSettings()` is called from 17+ files with 19+ call sites. A product grid with 10 products generates ~60 listeners from badges alone (each `ProductBadges` triggers 6 `useThemeSettings()` calls). Adding header, footer, logo, style, animation, newsletter, slideshow, and link components easily pushes past 100.
19+
20+
### Root Cause
21+
22+
1. **100 listener cap is too low** for real-world usage
23+
2. **Silent subscription drops** when cap is hit — components stop updating
24+
3. **Pilot badges.tsx** calls `useThemeSettings()` 6 times per product card (once in each badge component plus the shared `Badge` component)
25+
26+
React's `useSyncExternalStore` handles cleanup properly on unmount. The `Set` prevents duplicate listeners. The cap only breaks legitimate subscriptions.
27+
28+
## Approach: Remove Cap, Optimize Template
29+
30+
Chose over two alternatives:
31+
- **Nanostores migration**: Too large a change surface, breaks API, higher regression risk
32+
- **Selector pattern**: YAGNI — solving an unmeasured performance problem with massive scope creep
33+
34+
## Changes
35+
36+
### 1. ThemeSettingsStore — Remove Listener Cap
37+
38+
**File:** `packages/hydrogen/src/utils/use-theme-settings-store.ts`
39+
40+
- Remove `MAX_LISTENERS` constant
41+
- Remove the cap check that silently drops subscriptions
42+
- Add a dev-only warning that fires once when listener count exceeds 500 (advisory, never blocks)
43+
- Keep everything else: `Set<() => void>`, `isDestroyed` guards, `emit()` with snapshot copy, `destroy()`, singleton via `window.__weaverseThemeSettingsStore`
44+
45+
```typescript
46+
private hasWarnedListenerCount = false
47+
48+
subscribe = (callback: () => void) => {
49+
if (this.isDestroyed) {
50+
console.warn('ThemeSettingsStore: Cannot subscribe to destroyed store')
51+
return () => {}
52+
}
53+
54+
this.listeners.add(callback)
55+
56+
if (!this.hasWarnedListenerCount && this.listeners.size > 500) {
57+
this.hasWarnedListenerCount = true
58+
console.warn(
59+
`ThemeSettingsStore: ${this.listeners.size} listeners detected. ` +
60+
'This may indicate a performance issue. Consider using fewer useThemeSettings() calls.'
61+
)
62+
}
63+
64+
return () => {
65+
this.listeners.delete(callback)
66+
}
67+
}
68+
```
69+
70+
### 2. Pilot Template — Consolidate Badge useThemeSettings Calls
71+
72+
**File:** `templates/pilot/app/components/product/badges.tsx`
73+
74+
Move the single `useThemeSettings()` call to `ProductBadges` and pass settings as props to child badge components. This reduces 6 subscriptions per product card to 1.
75+
76+
- `ProductBadges` calls `useThemeSettings()` once, destructures all badge-related settings
77+
- Each badge component (`NewBadge`, `SaleBadge`, `BestSellerBadge`, `SoldOutBadge`, `BundleBadge`) receives settings via props
78+
- `Badge` becomes a pure presentational component (already receives style values as props)
79+
80+
### 3. Tests
81+
82+
**File:** `packages/hydrogen/src/utils/__tests__/use-theme-settings-store.test.ts`
83+
84+
Test cases:
85+
- Subscribe adds listener, unsubscribe removes it
86+
- No listener limit: 200+ listeners all receive updates
87+
- Destroyed store: subscribe returns no-op
88+
- Emit notifies all listeners
89+
- Cleanup: unsubscribed listener not called on update
90+
91+
## What's NOT Changing
92+
93+
- `WeaverseHydrogenRoot.tsx``useThemeSettings()` and `withWeaverse()` are correct
94+
- `@weaverse/react` or `@weaverse/core` — no changes needed
95+
- Other templates (Aspen, Naturelle) — benefit from SDK fix automatically
96+
- No selector pattern or store library migration

0 commit comments

Comments
 (0)