From f2a2365e74d7a37ba8a7b4f0974eddafd5b184e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Sat, 7 Feb 2026 21:29:55 +0530 Subject: [PATCH 01/11] make portals work with fabric --- .../Libraries/ReactNative/FabricUIManager.js | 2 + .../ReactNative/RendererImplementation.js | 3 + .../implementations/ReactFabric-dev.js | 36 +- .../Renderer/shims/ReactNativeTypes.js | 5 + .../react/renderer/uimanager/UIManager.cpp | 60 +- .../react/renderer/uimanager/UIManager.h | 6 + .../renderer/uimanager/UIManagerBinding.cpp | 22 + packages/rn-tester/js/RNTesterAppShared.js | 602 +++++++++--------- 8 files changed, 432 insertions(+), 304 deletions(-) diff --git a/packages/react-native/Libraries/ReactNative/FabricUIManager.js b/packages/react-native/Libraries/ReactNative/FabricUIManager.js index 40de0d069c752f..e6767b622c9745 100644 --- a/packages/react-native/Libraries/ReactNative/FabricUIManager.js +++ b/packages/react-native/Libraries/ReactNative/FabricUIManager.js @@ -43,6 +43,7 @@ export interface Spec { +appendChild: (parentNode: Node, child: Node) => Node; +appendChildToSet: (childSet: NodeSet, child: Node) => void; +completeRoot: (rootTag: RootTag, childSet: NodeSet) => void; + +mountPortalChildren: (targetTag: number, childSet: NodeSet) => void; +measure: ( node: Node | NativeElementReference, callback: MeasureOnSuccessCallback, @@ -114,6 +115,7 @@ const CACHED_PROPERTIES = [ 'appendChild', 'appendChildToSet', 'completeRoot', + 'mountPortalChildren', 'measure', 'measureInWindow', 'measureLayout', diff --git a/packages/react-native/Libraries/ReactNative/RendererImplementation.js b/packages/react-native/Libraries/ReactNative/RendererImplementation.js index 5874de821cf131..0d980d976ecbce 100644 --- a/packages/react-native/Libraries/ReactNative/RendererImplementation.js +++ b/packages/react-native/Libraries/ReactNative/RendererImplementation.js @@ -171,6 +171,9 @@ export const getPublicInstanceFromInternalInstanceHandle: ReactFabricType['getPu export const getPublicInstanceFromRootTag: ReactFabricType['getPublicInstanceFromRootTag'] = getFabricMethod('getPublicInstanceFromRootTag'); +export const createPortal: ReactFabricType['createPortal'] = + getFabricMethod('createPortal'); + export function isProfilingRenderer(): boolean { return Boolean(__DEV__); } diff --git a/packages/react-native/Libraries/Renderer/implementations/ReactFabric-dev.js b/packages/react-native/Libraries/Renderer/implementations/ReactFabric-dev.js index daab0e35a8e516..0c54b46545d31a 100644 --- a/packages/react-native/Libraries/Renderer/implementations/ReactFabric-dev.js +++ b/packages/react-native/Libraries/Renderer/implementations/ReactFabric-dev.js @@ -9091,10 +9091,11 @@ __DEV__ && pushHostContext(workInProgress); break; case 4: - pushHostContainer( - workInProgress, - workInProgress.stateNode.containerInfo - ); + var _containerInfo = workInProgress.stateNode.containerInfo; + if (_containerInfo._isPortal) { + _containerInfo.surfaceId = requiredContext(rootInstanceStackCursor.current).containerTag; + } + pushHostContainer(workInProgress, _containerInfo); break; case 10: pushProvider( @@ -9426,11 +9427,15 @@ __DEV__ && return null; case 13: return updateSuspenseComponent(current, workInProgress, renderLanes); - case 4: + case 4: { + var _containerInfo2 = workInProgress.stateNode.containerInfo; + if (_containerInfo2._isPortal) { + _containerInfo2.surfaceId = requiredContext(rootInstanceStackCursor.current).containerTag; + } return ( pushHostContainer( workInProgress, - workInProgress.stateNode.containerInfo + _containerInfo2 ), (returnFiber = workInProgress.pendingProps), null === current @@ -9448,6 +9453,7 @@ __DEV__ && ), workInProgress.child ); + } case 11: return updateForwardRef( current, @@ -10056,7 +10062,7 @@ __DEV__ && node: createNode( renderLanes, _type2.uiViewClassName, - current.containerTag, + current.surfaceId || current.containerTag, keepChildren, workInProgress ), @@ -15902,7 +15908,7 @@ __DEV__ && node: createNode( hostContext, "RCTRawText", - rootContainerInstance.containerTag, + rootContainerInstance.surfaceId || rootContainerInstance.containerTag, { text: text }, internalInstanceHandle ) @@ -15961,7 +15967,11 @@ __DEV__ && }; } function replaceContainerChildren(container, newChildren) { - completeRoot(container.containerTag, newChildren); + if (container._isPortal) { + mountPortalChildren(container.containerTag, newChildren); + } else { + completeRoot(container.containerTag, newChildren); + } } function nativeOnUncaughtError(error, errorInfo) { !1 !== @@ -18701,6 +18711,7 @@ __DEV__ && appendChildNode = _nativeFabricUIManage.appendChild, appendChildNodeToSet = _nativeFabricUIManage.appendChildToSet, completeRoot = _nativeFabricUIManage.completeRoot, + mountPortalChildren = _nativeFabricUIManage.mountPortalChildren, registerEventHandler = _nativeFabricUIManage.registerEventHandler, FabricDiscretePriority = _nativeFabricUIManage.unstable_DiscreteEventPriority, @@ -18925,9 +18936,14 @@ __DEV__ && return injectInternals(internals); })(); exports.createPortal = function (children, containerTag) { + var portalContainer = { + containerTag: containerTag, + publicInstance: null, + _isPortal: true + }; return createPortal$1( children, - containerTag, + portalContainer, null, 2 < arguments.length && void 0 !== arguments[2] ? arguments[2] : null ); diff --git a/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js b/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js index bd5cf5eaace7b0..518efb233a426a 100644 --- a/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js +++ b/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js @@ -200,6 +200,11 @@ export type ReactFabricType = { internalInstanceHandle: InternalInstanceHandle, ): PublicInstance | PublicTextInstance | null, getPublicInstanceFromRootTag(rootTag: number): PublicRootInstance | null, + createPortal( + children: React$MixedElement | null, + containerTag: number, + key?: ?string, + ): React$Portal, ... }; diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp index c1d12096c2fda5..bbe764b681dff4 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -21,6 +21,7 @@ #include +#include #include namespace { @@ -182,6 +183,20 @@ void UIManager::appendChild( componentDescriptor.appendChild(parentShadowNode, childShadowNode); } +static std::shared_ptr findShadowNodeByTagRecursively( + std::shared_ptr parentShadowNode, + Tag tag); + +void UIManager::mountPortalChildren( + Tag targetTag, + const ShadowNode::UnsharedListOfShared& portalChildren) { + if (portalChildren->empty()) { + activePortals_.erase(targetTag); + } else { + activePortals_[targetTag] = portalChildren; + } +} + void UIManager::completeSurface( SurfaceId surfaceId, const ShadowNode::UnsharedListOfShared& rootChildren, @@ -191,12 +206,54 @@ void UIManager::completeSurface( shadowTreeRegistry_.visit(surfaceId, [&](const ShadowTree& shadowTree) { auto result = shadowTree.commit( [&](const RootShadowNode& oldRootShadowNode) { - return std::make_shared( + auto newRoot = std::make_shared( oldRootShadowNode, ShadowNodeFragment{ .props = ShadowNodeFragment::propsPlaceholder(), .children = rootChildren, }); + + // Apply any active portal children + for (const auto& [targetTag, portalChildren] : activePortals_) { + auto targetNode = findShadowNodeByTagRecursively( + std::static_pointer_cast(newRoot), + targetTag); + + if (targetNode) { + auto clonedRoot = newRoot->cloneTree( + targetNode->getFamily(), + [&](const ShadowNode& oldNode) { + auto existingChildren = oldNode.getChildren(); + + std::unordered_set portalTags; + for (const auto& child : *portalChildren) { + portalTags.insert(child->getTag()); + } + + // Copy existing children, excluding any portal children + // that are already present (from a previous commit) + auto mergedChildren = std::make_shared>>(); + mergedChildren->reserve(existingChildren.size() + portalChildren->size()); + for (const auto& child : existingChildren) { + if (portalTags.find(child->getTag()) == portalTags.end()) { + mergedChildren->push_back(child); + } + } + + // Append portal children + for (const auto& child : *portalChildren) { + mergedChildren->push_back(child); + } + + return oldNode.clone({.children = mergedChildren}); + }); + if (clonedRoot) { + newRoot = std::static_pointer_cast(clonedRoot); + } + } + } + + return newRoot; }, commitOptions); @@ -272,6 +329,7 @@ ShadowTree::Unique UIManager::stopSurface(SurfaceId surfaceId) const { // Stop any ongoing animations. stopSurfaceForAnimationDelegate(surfaceId); + // Waiting for all concurrent commits to be finished and unregistering the // `ShadowTree`. auto shadowTree = getShadowTreeRegistry().remove(surfaceId); diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h index eac4b5d9a49a1a..63795b3123a98f 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h @@ -154,6 +154,10 @@ class UIManager final : public ShadowTreeDelegate { const ShadowNode::UnsharedListOfShared &rootChildren, ShadowTree::CommitOptions commitOptions); + void mountPortalChildren( + Tag targetTag, + const ShadowNode::UnsharedListOfShared &portalChildren); + void setIsJSResponder( const std::shared_ptr &shadowNode, bool isJSResponder, @@ -249,6 +253,8 @@ class UIManager final : public ShadowTreeDelegate { std::unique_ptr lazyShadowTreeRevisionConsistencyManager_; std::shared_ptr animationBackend_; + + std::unordered_map activePortals_; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index 5f661273a67834..53b108ad6c11e1 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -471,6 +471,28 @@ jsi::Value UIManagerBinding::get( }); } + if (methodName == "mountPortalChildren") { + auto paramCount = 2; + return jsi::Function::createFromHostFunction( + runtime, + name, + paramCount, + [uiManager, methodName, paramCount]( + jsi::Runtime& runtime, + const jsi::Value& /*thisValue*/, + const jsi::Value* arguments, + size_t count) -> jsi::Value { + validateArgumentCount(runtime, methodName, paramCount, count); + + auto targetTag = static_cast(arguments[0].asNumber()); + auto shadowNodeList = + shadowNodeListFromValue(runtime, arguments[1]); + uiManager->mountPortalChildren(targetTag, shadowNodeList); + + return jsi::Value::undefined(); + }); + } + if (methodName == "registerEventHandler") { auto paramCount = 1; return jsi::Function::createFromHostFunction( diff --git a/packages/rn-tester/js/RNTesterAppShared.js b/packages/rn-tester/js/RNTesterAppShared.js index 6a53954e3df65d..3dc4ef75a4ae99 100644 --- a/packages/rn-tester/js/RNTesterAppShared.js +++ b/packages/rn-tester/js/RNTesterAppShared.js @@ -8,318 +8,189 @@ * @format */ -import type {RNTesterModuleInfo, ScreenTypes} from './types/RNTesterTypes'; -import ReportFullyDrawnView from '../ReportFullyDrawnView/ReportFullyDrawnView'; -import RNTesterModuleContainer from './components/RNTesterModuleContainer'; -import RNTesterModuleList from './components/RNTesterModuleList'; -import RNTesterNavBar, {navBarHeight} from './components/RNTesterNavbar'; -import {RNTesterThemeContext, themes} from './components/RNTesterTheme'; -import RNTTitleBar from './components/RNTTitleBar'; -import {title as PlaygroundTitle} from './examples/Playground/PlaygroundExample'; -import RNTesterList from './utils/RNTesterList'; -import { - RNTesterNavigationActionsType, - RNTesterNavigationReducer, -} from './utils/RNTesterNavigationReducer'; -import { - Screens, - getExamplesListWithRecentlyUsed, - initialNavigationState, -} from './utils/testerStateUtils'; import * as React from 'react'; -import {useCallback, useEffect, useMemo, useReducer} from 'react'; -import { - BackHandler, - Button, - Linking, - NativeComponentRegistry, - Platform, - StatusBar, - StyleSheet, - View, - useColorScheme, - useWindowDimensions, -} from 'react-native'; +import { useState, useCallback } from 'react'; +import { View, Text, StyleSheet, TouchableOpacity, findNodeHandle, Button } from 'react-native'; +import { createPortal } from 'react-native/Libraries/ReactNative/RendererProxy'; -// In Bridgeless mode, in dev, enable static view config validator -if (global.RN$Bridgeless === true && __DEV__) { - NativeComponentRegistry.setRuntimeConfigProvider(() => { - return { - native: false, - verify: true, - }; - }); -} - -// RNTester App currently uses in memory storage for storing navigation state - -type BackButton = ({onBack: () => void}) => React.Node; +const ThemeContext = React.createContext < string > ('none'); -const RNTesterApp = ({ - testList, - customBackButton, -}: { - testList?: { - components?: Array, - apis?: Array, - }, - customBackButton?: BackButton, -}): React.Node => { - const [state, dispatch] = useReducer( - RNTesterNavigationReducer, - initialNavigationState, +const PortaledContent = ({ handleUnmount }) => { + const theme = React.useContext(ThemeContext); + console.log('PortaledContent', theme); + React.useEffect(() => { + console.log('PortaledContent', theme); + return () => { + console.log('PortaledContent unmounted'); + }; + }, [theme]); + return ( + + + I was rendered through a React Portal! + + + ThemeContext value: "{theme}" (proves context is preserved) + +