Description
Unmounting a screen that hosts KeyboardChatScrollView (e.g. popping it from a @react-navigation native stack) crashes the app with:
Error: Unable to find node on an unmounted component.
The throw comes from useScrollState's effect cleanup: useEventHandlerRegistration's returned cleanup re-resolves the view tag at teardown time (src/internal.ts):
return () => {
const viewTag = findNodeHandle(viewTagRef.current);
...
};
Two things combine here:
- React runs passive-effect cleanups for a deleted subtree after the fibers are detached.
- The ref involved is Reanimated's
useAnimatedRef (created internally by KeyboardChatScrollView — consumers can't influence it), and Reanimated's animated refs ignore the ref(null) call on detach, so viewTagRef.current still points at the unmounted instance.
findNodeHandle on that detached fiber then hits React's assertIsMounted invariant (in production this is minified invariant #188, so it's not a dev-only crash).
The code is identical in 1.22.2 (src, lib/module, lib/commonjs all re-resolve at cleanup), so upgrading doesn't help.
Steps to reproduce
- Render a
KeyboardChatScrollView (any content — we inject a Reanimated.FlatList via ScrollViewComponent, but the internal registration path is the same for the default ScrollView) inside a screen of a native stack navigator.
- Push the screen, then pop it (header back/close).
- Crash on unmount with the stack below.
Chat demos that keep the chat mounted at the tab root never pop the screen, which is probably why this hasn't surfaced in the example apps.
Stack trace (RN 0.85, Fabric, dev)
assertIsMounted (ReactNativeRenderer-dev.js:1321)
findCurrentFiberUsingSlowPath
findCurrentHostFiber
findHostInstanceWithWarning
findNodeHandle
<anonymous> (react-native-keyboard-controller/src/internal.ts:66)
<anonymous> (react-native-keyboard-controller/src/components/hooks/useScrollState.ts:53)
commitHookEffectListUnmount
commitPassiveUnmountEffectsInsideOfDeletedTree_begin
...
Environment
| package |
version |
| react-native-keyboard-controller |
1.21.6 (bug present in 1.22.2 sources too) |
| react-native |
0.85.3 (new architecture) |
| react-native-reanimated |
4.3.1 |
| expo |
56 |
| platform |
iOS |
Suggested fix
Capture the view tag once at registration and use the captured tag in the cleanup, instead of re-resolving through findNodeHandle at teardown — unregistering with the tag that was registered is also more precise if the ref were ever re-pointed.
Workaround we ship today (patch-package)
Guarding the library's own findNodeHandle wrapper so the invariant resolves to null (its documented "cannot resolve" result); the skipped unregister is moot since the native view is destroyed with the tree:
diff --git a/node_modules/react-native-keyboard-controller/lib/commonjs/utils/findNodeHandle/index.native.js b/node_modules/react-native-keyboard-controller/lib/commonjs/utils/findNodeHandle/index.native.js
index a252c3f..1c414c7 100644
--- a/node_modules/react-native-keyboard-controller/lib/commonjs/utils/findNodeHandle/index.native.js
+++ b/node_modules/react-native-keyboard-controller/lib/commonjs/utils/findNodeHandle/index.native.js
@@ -3,11 +3,17 @@
Object.defineProperty(exports, "__esModule", {
value: true
});
-Object.defineProperty(exports, "findNodeHandle", {
- enumerable: true,
- get: function () {
- return _reactNative.findNodeHandle;
- }
-});
+exports.findNodeHandle = void 0;
var _reactNative = require("react-native");
+
+// PATCHED (2026-07-29): see src/utils/findNodeHandle/index.native.ts — guards the
+// "Unable to find node on an unmounted component" throw during unmount-time cleanup.
+const findNodeHandle = componentOrHandle => {
+ try {
+ return _reactNative.findNodeHandle(componentOrHandle);
+ } catch {
+ return null;
+ }
+};
+exports.findNodeHandle = findNodeHandle;
//# sourceMappingURL=index.native.js.map
diff --git a/node_modules/react-native-keyboard-controller/lib/module/utils/findNodeHandle/index.native.js b/node_modules/react-native-keyboard-controller/lib/module/utils/findNodeHandle/index.native.js
index 8e64bcf..7555496 100644
--- a/node_modules/react-native-keyboard-controller/lib/module/utils/findNodeHandle/index.native.js
+++ b/node_modules/react-native-keyboard-controller/lib/module/utils/findNodeHandle/index.native.js
@@ -1,3 +1,13 @@
-import { findNodeHandle } from "react-native";
+import { findNodeHandle as findNodeHandleRN } from "react-native";
+
+// PATCHED (2026-07-29): see src/utils/findNodeHandle/index.native.ts — guards the
+// "Unable to find node on an unmounted component" throw during unmount-time cleanup.
+const findNodeHandle = componentOrHandle => {
+ try {
+ return findNodeHandleRN(componentOrHandle);
+ } catch {
+ return null;
+ }
+};
export { findNodeHandle };
//# sourceMappingURL=index.native.js.map
diff --git a/node_modules/react-native-keyboard-controller/src/utils/findNodeHandle/index.native.ts b/node_modules/react-native-keyboard-controller/src/utils/findNodeHandle/index.native.ts
index 55ac2fa..296a5c3 100644
--- a/node_modules/react-native-keyboard-controller/src/utils/findNodeHandle/index.native.ts
+++ b/node_modules/react-native-keyboard-controller/src/utils/findNodeHandle/index.native.ts
@@ -1,3 +1,18 @@
-import { findNodeHandle } from "react-native";
+import { findNodeHandle as findNodeHandleRN } from "react-native";
+
+// PATCHED (2026-07-29): RN's findNodeHandle THROWS "Unable to find node on an
+// unmounted component" when the fiber sits inside a deleted tree — exactly what
+// `useScrollState`'s effect cleanup does on screen unmount (`internal.ts` resolves
+// `viewTagRef.current` at teardown, and reanimated animated refs are never nulled on detach),
+// crashing the app whenever a screen hosting `KeyboardChatScrollView` is popped. Resolving to
+// `null` is findNodeHandle's documented "cannot resolve" contract; the unregister that follows a
+// null tag is skipped, which is correct — the native view is being destroyed anyway.
+const findNodeHandle: typeof findNodeHandleRN = (componentOrHandle) => {
+ try {
+ return findNodeHandleRN(componentOrHandle);
+ } catch {
+ return null;
+ }
+};
export { findNodeHandle };
Description
Unmounting a screen that hosts
KeyboardChatScrollView(e.g. popping it from a@react-navigationnative stack) crashes the app with:The throw comes from
useScrollState's effect cleanup:useEventHandlerRegistration's returned cleanup re-resolves the view tag at teardown time (src/internal.ts):Two things combine here:
useAnimatedRef(created internally byKeyboardChatScrollView— consumers can't influence it), and Reanimated's animated refs ignore theref(null)call on detach, soviewTagRef.currentstill points at the unmounted instance.findNodeHandleon that detached fiber then hits React'sassertIsMountedinvariant (in production this is minified invariant #188, so it's not a dev-only crash).The code is identical in
1.22.2(src,lib/module,lib/commonjsall re-resolve at cleanup), so upgrading doesn't help.Steps to reproduce
KeyboardChatScrollView(any content — we inject aReanimated.FlatListviaScrollViewComponent, but the internal registration path is the same for the default ScrollView) inside a screen of a native stack navigator.Chat demos that keep the chat mounted at the tab root never pop the screen, which is probably why this hasn't surfaced in the example apps.
Stack trace (RN 0.85, Fabric, dev)
Environment
Suggested fix
Capture the view tag once at registration and use the captured tag in the cleanup, instead of re-resolving through
findNodeHandleat teardown — unregistering with the tag that was registered is also more precise if the ref were ever re-pointed.Workaround we ship today (patch-package)
Guarding the library's own
findNodeHandlewrapper so the invariant resolves tonull(its documented "cannot resolve" result); the skipped unregister is moot since the native view is destroyed with the tree: