From 69f541814dd07a295461a32b13835cac102ad68a Mon Sep 17 00:00:00 2001 From: "jh.xiong" Date: Mon, 24 Aug 2026 23:33:55 +0800 Subject: [PATCH] Preserve lazy ViewManager initialization in UIManager Copy UIManager implementation property descriptors instead of spreading UIManagerImpl, so Bridgeless initialization does not eagerly invoke lazy ViewManager getters or trigger unnecessary getConstantsForViewManager calls. Add regression coverage verifying that ViewManager descriptors remain lazy, own, and enumerable, and that each getter is evaluated only once on first access. --- .../Libraries/ReactNative/UIManager.js | 19 ++++- .../ReactNative/__tests__/UIManager-test.js | 74 +++++++++++++++++++ 2 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 packages/react-native/Libraries/ReactNative/__tests__/UIManager-test.js diff --git a/packages/react-native/Libraries/ReactNative/UIManager.js b/packages/react-native/Libraries/ReactNative/UIManager.js index a97c6a7b3ea7..64d433c75975 100644 --- a/packages/react-native/Libraries/ReactNative/UIManager.js +++ b/packages/react-native/Libraries/ReactNative/UIManager.js @@ -23,9 +23,7 @@ const UIManagerImpl: UIManagerJSInterface = ? require('./BridgelessUIManager').default : require('./PaperUIManager').default; -// $FlowFixMe[cannot-spread-interface] -const UIManager: UIManagerJSInterface = { - ...UIManagerImpl, +const UIManagerOverrides = { /** * Determines the location on screen, width, and height of the given view and * returns the values via an async callback. If successful, the callback will @@ -243,4 +241,19 @@ const UIManager: UIManagerJSInterface = { }, }; +// Copy property descriptors instead of spreading UIManagerImpl. In Bridgeless +// mode, UIManagerImpl may define enumerable lazy ViewManager getters. Spreading +// the object would invoke every getter during module initialization. +// $FlowFixMe[incompatible-type] Flow cannot infer properties from descriptors. +const UIManager: UIManagerJSInterface = Object.create( + Object.getPrototypeOf(UIManagerImpl), + { + ...Object.getOwnPropertyDescriptors( + // $FlowFixMe[class-object-subtyping] UIManagerImpl is a runtime object. + UIManagerImpl, + ), + ...Object.getOwnPropertyDescriptors(UIManagerOverrides), + }, +); + export default UIManager; diff --git a/packages/react-native/Libraries/ReactNative/__tests__/UIManager-test.js b/packages/react-native/Libraries/ReactNative/__tests__/UIManager-test.js new file mode 100644 index 000000000000..9e2633eb48be --- /dev/null +++ b/packages/react-native/Libraries/ReactNative/__tests__/UIManager-test.js @@ -0,0 +1,74 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +jest.mock('../../Utilities/Platform', () => ({ + OS: 'android', + select: spec => spec.android ?? spec.native ?? spec.default, +})); +jest.unmock('../UIManager'); + +describe('UIManager', () => { + it('preserves lazy ViewManager getters when composing the Bridgeless implementation', () => { + const originalBridgelessValue = global.RN$Bridgeless; + const originalGetConstants = global.RN$LegacyInterop_UIManager_getConstants; + const originalGetConstantsForViewManager = + global.RN$LegacyInterop_UIManager_getConstantsForViewManager; + const originalGetDefaultEventTypes = + global.RN$LegacyInterop_UIManager_getDefaultEventTypes; + const viewManagerConfig = {Commands: {focus: 1}}; + const getConstants = jest.fn(() => ({ + LazyViewManagersEnabled: true, + ViewManagerNames: ['RCTTestView'], + })); + const getConstantsForViewManager = jest.fn(() => viewManagerConfig); + + jest.resetModules(); + try { + // $FlowExpectedError[cannot-write] + global.RN$Bridgeless = true; + // $FlowExpectedError[cannot-write] + global.RN$LegacyInterop_UIManager_getConstants = getConstants; + // $FlowExpectedError[cannot-write] + global.RN$LegacyInterop_UIManager_getConstantsForViewManager = + getConstantsForViewManager; + // $FlowExpectedError[cannot-write] + global.RN$LegacyInterop_UIManager_getDefaultEventTypes = jest.fn( + () => ({}), + ); + + const UIManager = require('../UIManager').default; + + expect(getConstants).toHaveBeenCalledTimes(1); + expect(getConstantsForViewManager).not.toHaveBeenCalled(); + expect( + Object.getOwnPropertyDescriptor(UIManager, 'RCTTestView'), + ).toBeDefined(); + expect(Object.keys(UIManager)).toContain('RCTTestView'); + + expect(Reflect.get(UIManager, 'RCTTestView')).toBe(viewManagerConfig); + expect(Reflect.get(UIManager, 'RCTTestView')).toBe(viewManagerConfig); + expect(getConstantsForViewManager).toHaveBeenCalledTimes(1); + } finally { + // $FlowExpectedError[cannot-write] + global.RN$Bridgeless = originalBridgelessValue; + // $FlowExpectedError[cannot-write] + global.RN$LegacyInterop_UIManager_getConstants = originalGetConstants; + // $FlowExpectedError[cannot-write] + global.RN$LegacyInterop_UIManager_getConstantsForViewManager = + originalGetConstantsForViewManager; + // $FlowExpectedError[cannot-write] + global.RN$LegacyInterop_UIManager_getDefaultEventTypes = + originalGetDefaultEventTypes; + jest.resetModules(); + } + }); +});