fix(ink): restore host prop updates in React 19 reconciler#589
Merged
Conversation
React 19's react-reconciler@0.33 mutation path calls commitUpdate with (instance, type, oldProps, newProps, fiber), but our Ink host config still expected an updatePayload from prepareUpdate. That left mounted ink-* nodes with stale onKeyDown, tabIndex, and textStyles, making menu navigation and highlights appear stuck until remount. Diff old/new props directly inside commitUpdate and add regression tests covering in-place updates for ink-box handlers/attributes and ink-text styles.
Contributor
|
thank you for this @catgirl3d ! |
gnanam1990
approved these changes
Apr 11, 2026
gnanam1990
left a comment
Collaborator
There was a problem hiding this comment.
Thanks for digging deeper on this. I pulled the current head locally and this looks like the right fix direction to me.
What I verified:
- the change now targets the actual shared root cause in src/ink/reconciler.ts instead of relying on remount-based workarounds in Box/Text
- commitUpdate now restores in-place host prop updates for ink-* nodes on the React 19 / react-reconciler@0.33 path
- the new low-level regression coverage in src/ink/reconciler.test.ts directly checks the important failure modes:
- ink-box updates onKeyDown and tabIndex in place across rerenders
- ink-text updates textStyles in place across rerenders
- handler removal and layout-style updates also behave correctly
- the behavior-level symptom checks also pass through the higher-level tests
Local verification on my side:
- bun test ./src/ink/reconciler.test.ts ./src/components/InteractiveMenuRegression.test.tsx ./src/components/ThemePicker.test.tsx
- bun run build
Both passed.
Maintainer summary: this is a stronger and more principled fix than the earlier workaround-style approaches in the same bug family, and I do not see a blocker on the current head.
Vasanthdev2004
pushed a commit
that referenced
this pull request
Apr 12, 2026
Rebase on current main (includes #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 #589 (which fixed the ink reconciler's commitUpdate path for host prop updates). #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.
euxaristia
pushed a commit
to euxaristia/openclaude
that referenced
this pull request
Apr 13, 2026
React 19's react-reconciler@0.33 mutation path calls commitUpdate with (instance, type, oldProps, newProps, fiber), but our Ink host config still expected an updatePayload from prepareUpdate. That left mounted ink-* nodes with stale onKeyDown, tabIndex, and textStyles, making menu navigation and highlights appear stuck until remount. Diff old/new props directly inside commitUpdate and add regression tests covering in-place updates for ink-box handlers/attributes and ink-text styles.
Franzferdinan51
pushed a commit
to Franzferdinan51/DuckHive
that referenced
this pull request
Apr 22, 2026
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.
C1ph3r404
pushed a commit
to C1ph3r404/openclaude
that referenced
this pull request
Apr 29, 2026
React 19's react-reconciler@0.33 mutation path calls commitUpdate with (instance, type, oldProps, newProps, fiber), but our Ink host config still expected an updatePayload from prepareUpdate. That left mounted ink-* nodes with stale onKeyDown, tabIndex, and textStyles, making menu navigation and highlights appear stuck until remount. Diff old/new props directly inside commitUpdate and add regression tests covering in-place updates for ink-box handlers/attributes and ink-text styles.
kevincodex1
pushed a commit
that referenced
this pull request
May 5, 2026
* fix(theme): remove stale React Compiler memo wrappers from theme hooks Rebase on current main (includes #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 #589 (which fixed the ink reconciler's commitUpdate path for host prop updates). #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>
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>
The-FOOL-00
pushed a commit
to The-FOOL-00/openclaude
that referenced
this pull request
May 24, 2026
React 19's react-reconciler@0.33 mutation path calls commitUpdate with (instance, type, oldProps, newProps, fiber), but our Ink host config still expected an updatePayload from prepareUpdate. That left mounted ink-* nodes with stale onKeyDown, tabIndex, and textStyles, making menu navigation and highlights appear stuck until remount. Diff old/new props directly inside commitUpdate and add regression tests covering in-place updates for ink-box handlers/attributes and ink-text styles.
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
React 19's react-reconciler@0.33 mutation path calls commitUpdate with (instance, type, oldProps, newProps, fiber), but our Ink host config still expected an updatePayload from prepareUpdate. That left mounted ink-* nodes with stale onKeyDown, tabIndex, and textStyles, making menu navigation and highlights appear stuck until remount. Diff old/new props directly inside commitUpdate and add regression tests covering in-place updates for ink-box handlers/attributes and ink-text styles.
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
It seems we finally found the real root cause. Problem wasn't in Box or Text, but in src/ink/reconciler.ts: after migrating to React 19 / react-reconciler@0.33 our commitUpdate was broken, so mounted ink-* nodes didn't update onKeyDown, tabIndex and textStyles in-place. That's why the previous fixes acted as workarounds via remounting/avoiding stale handlers. This is now fixed in the reconciler, low-level regression tests have been added, and the bug reproducibly disappears without those workarounds
Summary
ink-*nodes under React 19 /react-reconciler@0.33.ink-boxhandlers/attributes orink-textstyles stop updating on rerender.What we found
The menu bugs in
/agents,/config, and related screens looked like focus or repaint issues, but the shared failure mode was deeper: already-mountedink-*host nodes were not receiving updated props reliably.Minimal raw host-node probes showed that before the fix:
ink-boxkept staleonKeyDownhandlers across rerendersink-boxcould retain staletabIndexink-textcould retain staletextStylesThe root cause was in
src/ink/reconciler.ts: ourcommitUpdateimplementation still expected the olderprepareUpdate/updatePayloadshape, whilereact-reconciler@0.33.0mutation mode callscommitUpdate(instance, type, oldProps, newProps, fiber).That mismatch broke in-place host prop updates and made remount-based workarounds in
Box.tsxandText.tsxappear necessary.Implementation
src/ink/reconciler.tssocommitUpdatediffsoldPropsandnewPropsdirectly in the React 19 mutation path instead of expecting anupdatePayload.textStyles, and layout styles through the normal host update path.src/ink/reconciler.test.tswith low-level regression coverage for:ink-boxupdatingtabIndexandonKeyDownin place on rerenderink-textupdatingtextStylesin place on rerenderNotes
Box.tsxandText.tsxwere valid workarounds, but not the root fixTesting
bun test src/ink/reconciler.test.tsbun test src/components/InteractiveMenuRegression.test.tsx src/components/ThemePicker.test.tsxbun run buildbefore:

after:
bandicam.2026-04-11.01-51-25-796.mp4