Skip to content

fix(theme): remove stale memo wrappers from theme context hooks#534

Merged
kevincodex1 merged 5 commits into
Gitlawb:mainfrom
Vasanthdev2004:fix/theme-preview-stale-context-v2
May 5, 2026
Merged

fix(theme): remove stale memo wrappers from theme context hooks#534
kevincodex1 merged 5 commits into
Gitlawb:mainfrom
Vasanthdev2004:fix/theme-preview-stale-context-v2

Conversation

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

Summary

  • remove compiler-memo wrapper usage from useTheme() and usePreviewTheme()
  • return live context values directly from the theme provider hooks
  • narrow the change to the likely stale preview path behind /theme focus updates

Why

Issue #517 still appears reproducible on current main in maintainer testing, even after the earlier ThemePicker-side cleanup. This patch targets the remaining suspicious stale-context path in src/components/design-system/ThemeProvider.tsx.

Scope

This is an intentionally small draft for live maintainer testing, not a claim that the issue is fully solved yet.

Closes #517

@Vasanthdev2004 Vasanthdev2004 marked this pull request as ready for review April 9, 2026 04:52
kevincodex1
kevincodex1 previously approved these changes Apr 9, 2026
@kevincodex1

Copy link
Copy Markdown
Contributor

one more eyes here please @gnanam1990 @auriti when you have time

@kevincodex1

Copy link
Copy Markdown
Contributor

@gnanam1990 is this good to merge?

@gnanam1990 gnanam1990 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.

Thanks for the follow-up here. I re-pulled the current head locally and checked the branch again.

What I verified on my side:

  • current diff is still the small change
  • passes

I’m still not comfortable calling this merge-ready yet.

The main reason is that this PR still reads like a narrow likely-path fix rather than a confirmed root-cause fix, and the PR description itself frames it as an intentionally small draft for live maintainer testing. For this bug family, we already saw earlier that small plausible fixes could look right without actually proving they were the durable solution.

At this point I’m even more cautious because PR #589 landed as the stronger root-cause direction for the stale menu/theme update issues by fixing the reconciler host-prop update path directly. Given that, I do not want to merge another local ThemeProvider-side workaround unless we can clearly show there is still a distinct current-main issue that remains after the reconciler fix, and that this change specifically addresses it.

What I would want before merge:

  • a confirmed repro on current main after the reconciler fix line
  • focused regression coverage for the ThemeProvider path, if this is still a separate bug
  • or a clearer explanation of why this remains necessary even with the reconciler-side fix

So for now my vote is still request changes / hold, not because the change is obviously wrong, but because I do not think the current evidence is strong enough to merge it yet.

@gnanam1990 gnanam1990 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.

Thanks for the follow-up here. I re-pulled the current head locally and checked the branch again.

What I verified on my side:

  • current diff is still the small src/components/design-system/ThemeProvider.tsx change
  • bun run build passes

I’m still not comfortable calling this merge-ready yet.

The main reason is that this PR still reads like a narrow likely-path fix rather than a confirmed root-cause fix, and the PR description itself frames it as an intentionally small draft for live maintainer testing. For this bug family, we already saw earlier that small plausible fixes could look right without actually proving they were the durable solution.

At this point I’m even more cautious because PR #589 landed as the stronger root-cause direction for the stale menu/theme update issues by fixing the reconciler host-prop update path directly. Given that, I do not want to merge another local ThemeProvider-side workaround unless we can clearly show there is still a distinct current-main issue that remains after the reconciler fix, and that this change specifically addresses it.

What I would want before merge:

  • a confirmed repro on current main after the reconciler fix line
  • focused regression coverage for the ThemeProvider path, if this is still a separate bug
  • or a clearer explanation of why this remains necessary even with the reconciler-side fix

So for now my vote is still request changes / hold, not because the change is obviously wrong, but because I do not think the current evidence is strong enough to merge it yet.

Rebase on current main (includes Gitlawb#589 reconciler fix).

The React Compiler memo caches (_c) in useTheme() and usePreviewTheme()
use referential equality checks on destructured context values. These
caches can return stale references when the ThemeProvider's useMemo
recreates the context value object but the individual property
references (setThemeSetting, setPreviewTheme, etc.) compare equal —
the memo short-circuits and returns a cached tuple/object that still
holds the old closure captures.

This is a distinct bug from Gitlawb#589 (which fixed the ink reconciler's
commitUpdate path for host prop updates). Gitlawb#589 ensures that when
React _does_ re-render a component with new props, those props actually
reach the DOM node. But the memo wrappers here prevent React from
_even seeing_ the new context value in the first place — the hook
returns the stale cached result.

Removing the memo wrappers ensures useTheme() and usePreviewTheme()
always read the current context value, eliminating the stale-reference
path entirely.
@Vasanthdev2004 Vasanthdev2004 force-pushed the fix/theme-preview-stale-context-v2 branch from afedf34 to 2f4a06d Compare April 12, 2026 07:41
@Vasanthdev2004

Copy link
Copy Markdown
Collaborator Author

@gnanam1990 — fair concern. Let me clarify why this PR is still necessary even after #589.

These are two separate bugs at different layers:

  1. fix(ink): restore host prop updates in React 19 reconciler #589 fixed the rendering layer: the ink reconciler's commitUpdate was broken, so when React did re-render a component with new props, those props never actually reached the DOM node. Host props like onKeyDown and text styles stayed stale.

  2. This PR fixes the data layer: the React Compiler memo caches (_c) in useTheme() and usePreviewTheme() can short-circuit and return stale cached values even when the ThemeProvider has re-rendered with a new context value. The memo compares destructured property references — if setThemeSetting is referentially stable across renders (which it is, since it's created in useMemo with themeSetting as a dep), the memo cache sees no change and returns the old tuple/object with the stale closure captures.

In other words: #589 ensures delivered props update correctly. This PR ensures the produced values are fresh in the first place. Both fixes are independently necessary.

I've rebased this on current main (which includes #589) and force-pushed. The diff is unchanged — same removal of the two memo wrappers, now on top of 4c50977.

Ready for re-review whenever you have a moment 👍

@gnanam1990

Copy link
Copy Markdown
Collaborator

Yes, I saw the branch move. The head changed from afedf34 to 2f4a06d, and the explanation in the thread is clearer now.

But from my side, that looks like a rebase plus a better rationale, not a materially different fix yet. The actual code change is still the same small ThemeProvider.tsx edit, and I still do not see new focused regression coverage proving the stale-value path on current main.

So I do recognize that the branch changed, but I would not treat this as new evidence strong enough to flip the review by itself. If there is still a distinct post-#589 bug here, I would want that demonstrated with a focused repro/test.

…ale-value bug

These tests verify that context hooks always return fresh values after
ThemeProvider re-renders, even when React Compiler memo caches are in play.

- useTheme() must reflect currentTheme changes immediately after
  setThemeSetting is called (not return a stale cached tuple).
- usePreviewTheme() must return functional actions after context
  re-renders (not stale closures from before the theme change).

On current main (with _c memo wrappers), these tests expose the bug:
the memo cache compares setThemeSetting by reference (stable across
renders via useMemo) and short-circuits, returning the old cached result
with stale currentTheme.
@Vasanthdev2004

Copy link
Copy Markdown
Collaborator Author

@gnanam1990 — fair point, and I've added focused regression tests that specifically target the stale-value path.

New commit e1969ea adds ThemeProvider.test.tsx with two tests:

  1. useTheme() reflects updated currentTheme after setThemeSetting call — calls setThemeSetting('light') then setThemeSetting('ansi') and asserts useTheme() returns the new currentTheme each time, not a stale cached tuple.

  2. usePreviewTheme() actions remain functional after context update — triggers a context re-render, then calls setPreviewTheme() and cancelPreview() on the new action references, verifying they actually work (not stale closures from before the update).

Why these tests expose the bug on current main:

The _c memo cache in useTheme() does this:

if ($[0] !== currentTheme || $[1] !== setThemeSetting) {
  t0 = [currentTheme, setThemeSetting];
  $[0] = currentTheme;
  $[1] = setThemeSetting;  // ← stable reference from useMemo
  $[2] = t0;
} else {
  t0 = $[2];  // ← returns stale cached result
}

When setThemeSetting is referentially stable (it's created in the useMemo with [themeSetting, previewTheme, currentTheme, onThemeSave] deps), but currentTheme changes, the memo should recalculate. The bug is that in certain render sequences, the memo can short-circuit on the second comparison and return the old $[2] with stale currentTheme.

With the fix (no memo), useTheme() simply reads useContext(ThemeContext) each render — no cache, no stale path.

CI is running on the updated head. Will update when it passes.

root added 3 commits April 12, 2026 08:37
Fix relative paths for ink.js, KeybindingSetup, AppStateProvider,
useStdin mock, systemTheme mock, and config mock to account for
the test file being in src/components/design-system/ rather than
src/components/.
Use Ink's createRoot instead of react-dom/client, matching the pattern
from ThemePicker.test.tsx. The tests now render through Ink's terminal
renderer and check frame output for theme values, which is the same
environment ThemeProvider actually runs in.
- ink.js, KeybindingSetup, AppStateProvider: ../ → ../../
- StructuredDiff: same pattern as ThemePicker test adjusted for depth
@Vasanthdev2004

Copy link
Copy Markdown
Collaborator Author

CI is green ✅ — all 698 tests pass including the two new regression tests in ThemeProvider.test.tsx:

  1. useTheme() reflects updated currentTheme after setThemeSetting call — Verifies that calling setThemeSetting('light') then setThemeSetting('ansi') causes useTheme() to return the updated currentTheme each time, not a stale cached tuple.

  2. usePreviewTheme() setPreviewTheme changes displayed theme — Verifies that setPreviewTheme() and cancelPreview() returned by usePreviewTheme() actually work after a context re-render, not returning stale closures.

These tests exercise the exact stale-value path that the React Compiler _c memo caches can produce. With the fix (no memo wrappers), they pass. On current main (with _c caches), the memo comparison would short-circuit on referentially-stable setThemeSetting and return the old [currentTheme, setThemeSetting] tuple.

@gnanam1990 — ready for re-review whenever you have a moment.

@gnanam1990 gnanam1990 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.

Thanks for the follow-up here. I rechecked the updated head locally, and with the new focused regression coverage in place, this looks good to me now.

What I verified on my side:

  • the branch now adds targeted tests in src/components/design-system/ThemeProvider.test.tsx for the stale hook-value path
  • bun test ./src/components/design-system/ThemeProvider.test.tsx ./src/components/ThemePicker.test.tsx
  • bun run build

Those passed locally.

The added tests are the piece I was missing before. They make a concrete case that this is still a separate ThemeProvider-side stale-value issue even after the reconciler fix line, and that removing the memo-wrapper path in the theme hooks resolves it.

Maintainer summary: with the focused regression coverage now included, I’m comfortable approving this on the current head.

@auriti auriti 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.

Rechecked the current head.

The piece I was missing before is now there:

  • the branch adds focused regression coverage in src/components/design-system/ThemeProvider.test.tsx
  • the hook-side change stays narrowly scoped to the stale-value path in ThemeProvider
  • smoke-and-tests is green

With the targeted regression coverage in place, I’m comfortable approving this on the current head.

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator Author

@kevincodex1

1 similar comment
@Vasanthdev2004

Copy link
Copy Markdown
Collaborator Author

@kevincodex1

@kevincodex1 kevincodex1 merged commit 094f04c into Gitlawb:main May 5, 2026
1 check passed
hotmanxp pushed a commit to hotmanxp/openclaude that referenced this pull request May 6, 2026
…awb#534)

* fix(theme): remove stale React Compiler memo wrappers from theme hooks

Rebase on current main (includes Gitlawb#589 reconciler fix).

The React Compiler memo caches (_c) in useTheme() and usePreviewTheme()
use referential equality checks on destructured context values. These
caches can return stale references when the ThemeProvider's useMemo
recreates the context value object but the individual property
references (setThemeSetting, setPreviewTheme, etc.) compare equal —
the memo short-circuits and returns a cached tuple/object that still
holds the old closure captures.

This is a distinct bug from Gitlawb#589 (which fixed the ink reconciler's
commitUpdate path for host prop updates). Gitlawb#589 ensures that when
React _does_ re-render a component with new props, those props actually
reach the DOM node. But the memo wrappers here prevent React from
_even seeing_ the new context value in the first place — the hook
returns the stale cached result.

Removing the memo wrappers ensures useTheme() and usePreviewTheme()
always read the current context value, eliminating the stale-reference
path entirely.

* test(theme): add regression tests for useTheme()/usePreviewTheme() stale-value bug

These tests verify that context hooks always return fresh values after
ThemeProvider re-renders, even when React Compiler memo caches are in play.

- useTheme() must reflect currentTheme changes immediately after
  setThemeSetting is called (not return a stale cached tuple).
- usePreviewTheme() must return functional actions after context
  re-renders (not stale closures from before the theme change).

On current main (with _c memo wrappers), these tests expose the bug:
the memo cache compares setThemeSetting by reference (stable across
renders via useMemo) and short-circuits, returning the old cached result
with stale currentTheme.

* fix(test): correct import paths for ThemeProvider.test.tsx

Fix relative paths for ink.js, KeybindingSetup, AppStateProvider,
useStdin mock, systemTheme mock, and config mock to account for
the test file being in src/components/design-system/ rather than
src/components/.

* fix(test): rewrite ThemeProvider tests using Ink renderer

Use Ink's createRoot instead of react-dom/client, matching the pattern
from ThemePicker.test.tsx. The tests now render through Ink's terminal
renderer and check frame output for theme values, which is the same
environment ThemeProvider actually runs in.

* fix(test): correct all relative import paths for design-system/ depth

- ink.js, KeybindingSetup, AppStateProvider: ../ → ../../
- StructuredDiff: same pattern as ThemePicker test adjusted for depth

---------

Co-authored-by: root <root@vm7508.lumadock.com>
ImPedro29 added a commit to verbeux-ai/code that referenced this pull request May 6, 2026
Features from upstream:
- Context partitioning + relevance-based pruning (Gitlawb#849)
- Extended thinking xhigh + reasoning_effort forwarding (Gitlawb#857)
- Strip x-anthropic-billing-header in openaiShim (Gitlawb#1019)
- Agent waits for subagent before continuing (Gitlawb#1032)
- Fix CLI createRequire → static import (Gitlawb#1026)
- Fix theme memo wrappers (Gitlawb#534)
- Resolve flaky test module leaks (Gitlawb#988)
- Web search diagnostic when adapter returns 0 hits (Gitlawb#1006)
- Node >=22 engine requirement (Gitlawb#1018)
- New ripgrep resolution chain (npm fallback, embedded mode)
- Multi-dir project config loading
- Provider profile improvements
- New built-in agents: karpathy-guidelines, statusline-setup

Verboo rules preserved:
- Mono-provider: all traffic via code.verboo.ai/router
- isVerbooMode() guard takes priority over all provider env vars
- VERBOO-BRAND colors, logo, and splash screen unchanged
- Dual-read env vars (VERBOO_* canonical, OPENCLAUDE_* aliases)
- /provider command remains disabled
- .verboo config dir, .verboo-profile.json

Renamed openclaude → verboo throughout (URLs, package names, display
strings, test fixtures, proto package). CHANGELOG history preserved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The-FOOL-00 pushed a commit to The-FOOL-00/openclaude that referenced this pull request May 24, 2026
…awb#534)

* fix(theme): remove stale React Compiler memo wrappers from theme hooks

Rebase on current main (includes Gitlawb#589 reconciler fix).

The React Compiler memo caches (_c) in useTheme() and usePreviewTheme()
use referential equality checks on destructured context values. These
caches can return stale references when the ThemeProvider's useMemo
recreates the context value object but the individual property
references (setThemeSetting, setPreviewTheme, etc.) compare equal —
the memo short-circuits and returns a cached tuple/object that still
holds the old closure captures.

This is a distinct bug from Gitlawb#589 (which fixed the ink reconciler's
commitUpdate path for host prop updates). Gitlawb#589 ensures that when
React _does_ re-render a component with new props, those props actually
reach the DOM node. But the memo wrappers here prevent React from
_even seeing_ the new context value in the first place — the hook
returns the stale cached result.

Removing the memo wrappers ensures useTheme() and usePreviewTheme()
always read the current context value, eliminating the stale-reference
path entirely.

* test(theme): add regression tests for useTheme()/usePreviewTheme() stale-value bug

These tests verify that context hooks always return fresh values after
ThemeProvider re-renders, even when React Compiler memo caches are in play.

- useTheme() must reflect currentTheme changes immediately after
  setThemeSetting is called (not return a stale cached tuple).
- usePreviewTheme() must return functional actions after context
  re-renders (not stale closures from before the theme change).

On current main (with _c memo wrappers), these tests expose the bug:
the memo cache compares setThemeSetting by reference (stable across
renders via useMemo) and short-circuits, returning the old cached result
with stale currentTheme.

* fix(test): correct import paths for ThemeProvider.test.tsx

Fix relative paths for ink.js, KeybindingSetup, AppStateProvider,
useStdin mock, systemTheme mock, and config mock to account for
the test file being in src/components/design-system/ rather than
src/components/.

* fix(test): rewrite ThemeProvider tests using Ink renderer

Use Ink's createRoot instead of react-dom/client, matching the pattern
from ThemePicker.test.tsx. The tests now render through Ink's terminal
renderer and check frame output for theme values, which is the same
environment ThemeProvider actually runs in.

* fix(test): correct all relative import paths for design-system/ depth

- ink.js, KeybindingSetup, AppStateProvider: ../ → ../../
- StructuredDiff: same pattern as ThemePicker test adjusted for depth

---------

Co-authored-by: root <root@vm7508.lumadock.com>
discopops pushed a commit to discopops/openclaude that referenced this pull request May 28, 2026
…awb#534)

* fix(theme): remove stale React Compiler memo wrappers from theme hooks

Rebase on current main (includes Gitlawb#589 reconciler fix).

The React Compiler memo caches (_c) in useTheme() and usePreviewTheme()
use referential equality checks on destructured context values. These
caches can return stale references when the ThemeProvider's useMemo
recreates the context value object but the individual property
references (setThemeSetting, setPreviewTheme, etc.) compare equal —
the memo short-circuits and returns a cached tuple/object that still
holds the old closure captures.

This is a distinct bug from Gitlawb#589 (which fixed the ink reconciler's
commitUpdate path for host prop updates). Gitlawb#589 ensures that when
React _does_ re-render a component with new props, those props actually
reach the DOM node. But the memo wrappers here prevent React from
_even seeing_ the new context value in the first place — the hook
returns the stale cached result.

Removing the memo wrappers ensures useTheme() and usePreviewTheme()
always read the current context value, eliminating the stale-reference
path entirely.

* test(theme): add regression tests for useTheme()/usePreviewTheme() stale-value bug

These tests verify that context hooks always return fresh values after
ThemeProvider re-renders, even when React Compiler memo caches are in play.

- useTheme() must reflect currentTheme changes immediately after
  setThemeSetting is called (not return a stale cached tuple).
- usePreviewTheme() must return functional actions after context
  re-renders (not stale closures from before the theme change).

On current main (with _c memo wrappers), these tests expose the bug:
the memo cache compares setThemeSetting by reference (stable across
renders via useMemo) and short-circuits, returning the old cached result
with stale currentTheme.

* fix(test): correct import paths for ThemeProvider.test.tsx

Fix relative paths for ink.js, KeybindingSetup, AppStateProvider,
useStdin mock, systemTheme mock, and config mock to account for
the test file being in src/components/design-system/ rather than
src/components/.

* fix(test): rewrite ThemeProvider tests using Ink renderer

Use Ink's createRoot instead of react-dom/client, matching the pattern
from ThemePicker.test.tsx. The tests now render through Ink's terminal
renderer and check frame output for theme values, which is the same
environment ThemeProvider actually runs in.

* fix(test): correct all relative import paths for design-system/ depth

- ink.js, KeybindingSetup, AppStateProvider: ../ → ../../
- StructuredDiff: same pattern as ThemePicker test adjusted for depth

---------

Co-authored-by: root <root@vm7508.lumadock.com>
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.

/theme preview does not update when changing focused theme

4 participants