From 285ddb9e3c21558f0246dfa7e0a705eb7a561f70 Mon Sep 17 00:00:00 2001 From: Furkan Dogan Date: Sun, 20 Sep 2026 10:15:15 +0300 Subject: [PATCH] fix(ios): dispatch chart commands through native refs (PRO-4970) --- README.md | 23 ++++++++++++++--------- index.js | 21 ++++++--------------- tests/commands.test.cjs | 25 +++++++++++++------------ 3 files changed, 33 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 6fc4efb..a0325b0 100644 --- a/README.md +++ b/README.md @@ -5,20 +5,25 @@ ### Native command dispatch -On Android, imperative chart commands use `codegenNativeCommands` with the +On Android and iOS, imperative chart commands use `codegenNativeCommands` with the current native host ref. Keep this path ref-based: converting the ref with `findNodeHandle` and calling `UIManager.dispatchViewManagerCommand` sends Fabric -through `findShadowNodeByTag_DEPRECATED`. React Native 0.85.3's default lookup can -race native tree teardown (PRO-4970). Ref-based dispatch avoids that lookup for -chart commands without changing global React Native feature flags. - -iOS retains its existing numeric command mapping. Both paths check the current -ref at call time and ignore commands after detachment. The native command names, -argument order and payloads are unchanged. +through `findShadowNodeByTag_DEPRECATED`. That lookup walks the whole shadow tree +on the JS thread and can race concurrent commits and native tree teardown +(PRO-4970); React Native only fixes it behind the +`fixFindShadowNodeByTagRaceCondition` flag, which is off by default. Ref-based +dispatch avoids the lookup for chart commands without changing global React +Native feature flags. + +On iOS the view is a legacy view manager running through Fabric's interop layer, +which resolves a command by its method name and prepends the view tag itself, so +the native `RCT_EXTERN_METHOD` signatures are unchanged. Both platforms check the +current ref at call time and ignore commands after detachment. The native command +names, argument order and payloads are unchanged. Run `yarn test` (Node 22+) for command routing, payload, detachment and independent chart checks. These tests mock the native bridge; they do **not** prove the -intermittent Android SIGSEGV is resolved. Validate a mobile build containing this +intermittent Android or iOS SIGSEGV is resolved. Validate a mobile build containing this package with repeated interval changes (including 3D → weekly), market and engine switches, reconnects, background/foreground and idle runs, collecting native logs. diff --git a/index.js b/index.js index 8140d86..ad7d12b 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ import React, { forwardRef, useImperativeHandle, useRef } from 'react'; -import { requireNativeComponent, UIManager, findNodeHandle, Platform } from 'react-native'; +import { requireNativeComponent } from 'react-native'; import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands'; const NativeRNKLineView = requireNativeComponent('RNKLineView'); @@ -26,20 +26,11 @@ const RNKLineView = forwardRef((props, ref) => { const view = nativeRef.current; if (!view) return detachedResult; - if (Platform.OS === 'android') { - // Dispatch through the host ref. Numeric tags force Fabric through - // findShadowNodeByTag_DEPRECATED, whose RN 0.85 lookup can race teardown. - return Commands[name](view, ...args); - } - - // Preserve iOS's exported numeric command mapping. - const nodeHandle = findNodeHandle(view); - if (!nodeHandle) return detachedResult; - return UIManager.dispatchViewManagerCommand( - nodeHandle, - UIManager.getViewManagerConfig('RNKLineView').Commands[name], - args - ); + // Dispatch through the host ref on every platform. Numeric tags force Fabric + // through findShadowNodeByTag_DEPRECATED, whose tree walk can race concurrent + // commits and native tree teardown (PRO-4970). iOS's legacy view manager + // interop resolves these commands by method name. + return Commands[name](view, ...args); }; useImperativeHandle(ref, () => ({ diff --git a/tests/commands.test.cjs b/tests/commands.test.cjs index f654bc9..dd23bfb 100644 --- a/tests/commands.test.cjs +++ b/tests/commands.test.cjs @@ -60,18 +60,12 @@ for (const platform of ['android', 'ios']) { const payload = { time: 123, close: 42 }; const args = name.startsWith('get') ? [] : [payload]; api[name](...args); - if (platform === 'android') { - assert.equal(calls.length, 1); - assert.equal(calls[0][0], 'host'); - assert.equal(calls[0][1], host); - assert.equal(calls[0][2], name); - } else { - assert.equal(calls.length, 2); - assert.equal(calls[0][0], 'lookup'); - assert.equal(calls[1][0], 'legacy'); - assert.equal(calls[1][1], host.tag); - assert.equal(calls[1][2], ids[name]); - } + // Both platforms must use the host ref: a numeric tag would send Fabric + // through findShadowNodeByTag_DEPRECATED. + assert.equal(calls.length, 1); + assert.equal(calls[0][0], 'host'); + assert.equal(calls[0][1], host); + assert.equal(calls[0][2], name); const sent = calls.at(-1)[3]; assert.equal(sent.length, args.length); if (args.length) assert.equal(sent[0], payload); @@ -95,6 +89,13 @@ for (const platform of ['android', 'ios']) { api.updateLastCandlestick({ close: 12 }); assert.equal(calls[0][1], replacement); }); + + test(`${platform}: no command resolves a numeric tag or uses the legacy dispatcher`, async () => { + const { api, calls } = await mount(platform); + for (const name of names) api[name]({ time: 123 }); + assert.equal(calls.length, names.length); + assert.ok(calls.every(([kind]) => kind === 'host')); + }); } test('independent charts dispatch to their own native refs', async () => {