diff --git a/packages/react-native/Libraries/Components/ScrollView/__tests__/ScrollView-viewCulling-itest.js b/packages/react-native/Libraries/Components/ScrollView/__tests__/ScrollView-viewCulling-itest.js
new file mode 100644
index 00000000000000..39f0992271acbb
--- /dev/null
+++ b/packages/react-native/Libraries/Components/ScrollView/__tests__/ScrollView-viewCulling-itest.js
@@ -0,0 +1,660 @@
+/**
+ * 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
+ * @oncall react_native
+ * @fantom_flags enableAccessToHostTreeInFabric:true
+ * @fantom_flags enableViewCulling:true
+ * @fantom_flags enableSynchronousStateUpdates:true
+ */
+
+import '../../../Core/InitializeCore.js';
+import ensureInstance from '../../../../src/private/utilities/ensureInstance';
+import ReactNativeElement from '../../../../src/private/webapis/dom/nodes/ReactNativeElement';
+import View from '../../View/View';
+import ScrollView from '../ScrollView';
+import Fantom from '@react-native/fantom';
+import * as React from 'react';
+
+test('basic culling', () => {
+ const root = Fantom.createRoot({viewportWidth: 100, viewportHeight: 100});
+ let maybeNode;
+
+ Fantom.runTask(() => {
+ root.render(
+ {
+ maybeNode = node;
+ }}>
+
+ ,
+ );
+ });
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "RootView", nativeID: (root)}',
+ 'Create {type: "ScrollView", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: "child"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: (N/A)}',
+ 'Insert {type: "ScrollView", parentNativeID: (root), index: 0, nativeID: (N/A)}',
+ ]);
+
+ const element = ensureInstance(maybeNode, ReactNativeElement);
+
+ Fantom.runOnUIThread(() => {
+ Fantom.scrollTo(element, {
+ x: 0,
+ y: 60,
+ });
+ });
+ Fantom.runWorkLoop();
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}',
+ 'Delete {type: "View", nativeID: "child"}',
+ 'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: (N/A)}',
+ 'Delete {type: "View", nativeID: (N/A)}',
+ 'Update {type: "ScrollView", nativeID: (N/A)}',
+ ]);
+
+ Fantom.runOnUIThread(() => {
+ Fantom.scrollTo(element, {
+ x: 0,
+ y: 0,
+ });
+ });
+ Fantom.runWorkLoop();
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "ScrollView", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: "child"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: (N/A)}',
+ ]);
+});
+
+test('recursive culling', () => {
+ const root = Fantom.createRoot({viewportHeight: 100, viewportWidth: 100});
+ let maybeNode;
+
+ Fantom.runTask(() => {
+ root.render(
+ {
+ maybeNode = node;
+ }}>
+
+
+
+
+
+
+
+
+ ,
+ );
+ });
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "RootView", nativeID: (root)}',
+ 'Create {type: "ScrollView", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: "element A"}',
+ 'Create {type: "View", nativeID: "child AA"}',
+ 'Create {type: "View", nativeID: "child AB"}',
+ 'Insert {type: "View", parentNativeID: "element A", index: 0, nativeID: "child AA"}',
+ 'Insert {type: "View", parentNativeID: "element A", index: 1, nativeID: "child AB"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "element A"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: (N/A)}',
+ 'Insert {type: "ScrollView", parentNativeID: (root), index: 0, nativeID: (N/A)}',
+ ]);
+
+ const element = ensureInstance(maybeNode, ReactNativeElement);
+
+ // === Scroll down to the edge of child AA ===
+ Fantom.runOnUIThread(() => {
+ Fantom.scrollTo(element, {
+ x: 0,
+ y: 30,
+ });
+ });
+ Fantom.runWorkLoop();
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "ScrollView", nativeID: (N/A)}',
+ ]);
+
+ // === Scroll down past child AA ===
+ Fantom.runOnUIThread(() => {
+ Fantom.scrollTo(element, {
+ x: 0,
+ y: 36,
+ });
+ });
+ Fantom.runWorkLoop();
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "ScrollView", nativeID: (N/A)}',
+ 'Remove {type: "View", parentNativeID: "element A", index: 0, nativeID: "child AA"}',
+ 'Delete {type: "View", nativeID: "child AA"}',
+ ]);
+
+ // === Scroll down past child AB ===
+ Fantom.runOnUIThread(() => {
+ Fantom.scrollTo(element, {
+ x: 0,
+ y: 51,
+ });
+ });
+ Fantom.runWorkLoop();
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "ScrollView", nativeID: (N/A)}',
+ 'Remove {type: "View", parentNativeID: "element A", index: 0, nativeID: "child AB"}',
+ 'Delete {type: "View", nativeID: "child AB"}',
+ ]);
+
+ // === Scroll down past element A ===
+ Fantom.runOnUIThread(() => {
+ Fantom.scrollTo(element, {
+ x: 0,
+ y: 56,
+ });
+ });
+ Fantom.runWorkLoop();
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "ScrollView", nativeID: (N/A)}',
+ 'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: "element A"}',
+ 'Delete {type: "View", nativeID: "element A"}',
+ ]);
+
+ // Scroll element B into viewport. Just child BA should be created.
+ Fantom.runOnUIThread(() => {
+ Fantom.scrollTo(element, {
+ x: 0,
+ y: 155,
+ });
+ });
+ Fantom.runWorkLoop();
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "ScrollView", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: "element B"}',
+ 'Create {type: "View", nativeID: "child BA"}',
+ 'Insert {type: "View", parentNativeID: "element B", index: 0, nativeID: "child BA"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "element B"}',
+ ]);
+
+ // Scroll child BA into viewport.
+ Fantom.runOnUIThread(() => {
+ Fantom.scrollTo(element, {
+ x: 0,
+ y: 165,
+ });
+ });
+ Fantom.runWorkLoop();
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "ScrollView", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: "child BB"}',
+ 'Insert {type: "View", parentNativeID: "element B", index: 1, nativeID: "child BB"}',
+ ]);
+
+ // Scroll back to start
+ Fantom.runOnUIThread(() => {
+ Fantom.scrollTo(element, {
+ x: 0,
+ y: 0,
+ });
+ });
+ Fantom.runWorkLoop();
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "ScrollView", nativeID: (N/A)}',
+ 'Remove {type: "View", parentNativeID: "element B", index: 1, nativeID: "child BB"}',
+ 'Remove {type: "View", parentNativeID: "element B", index: 0, nativeID: "child BA"}',
+ 'Delete {type: "View", nativeID: "child BA"}',
+ 'Delete {type: "View", nativeID: "child BB"}',
+ 'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: "element B"}',
+ 'Delete {type: "View", nativeID: "element B"}',
+ 'Create {type: "View", nativeID: "element A"}',
+ 'Create {type: "View", nativeID: "child AA"}',
+ 'Create {type: "View", nativeID: "child AB"}',
+ 'Insert {type: "View", parentNativeID: "element A", index: 0, nativeID: "child AA"}',
+ 'Insert {type: "View", parentNativeID: "element A", index: 1, nativeID: "child AB"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "element A"}',
+ ]);
+
+ // Scroll past element A
+ Fantom.runOnUIThread(() => {
+ Fantom.scrollTo(element, {
+ x: 0,
+ y: 85,
+ });
+ });
+ Fantom.runWorkLoop();
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "ScrollView", nativeID: (N/A)}',
+ 'Remove {type: "View", parentNativeID: "element A", index: 1, nativeID: "child AB"}',
+ 'Remove {type: "View", parentNativeID: "element A", index: 0, nativeID: "child AA"}',
+ 'Delete {type: "View", nativeID: "child AA"}',
+ 'Delete {type: "View", nativeID: "child AB"}',
+ 'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: "element A"}',
+ 'Delete {type: "View", nativeID: "element A"}',
+ ]);
+});
+
+test('recursive culling when initial offset is negative', () => {
+ const root = Fantom.createRoot({viewportHeight: 874, viewportWidth: 402});
+ let maybeNode;
+
+ Fantom.runTask(() => {
+ root.render(
+ {
+ maybeNode = node;
+ }}>
+
+
+
+
+
+ ,
+ );
+ });
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "RootView", nativeID: (root)}',
+ 'Create {type: "ScrollView", nativeID: (N/A)}',
+ 'Insert {type: "ScrollView", parentNativeID: (root), index: 0, nativeID: (N/A)}',
+ ]);
+
+ const element = ensureInstance(maybeNode, ReactNativeElement);
+
+ Fantom.runOnUIThread(() => {
+ Fantom.scrollTo(element, {
+ x: 0,
+ y: 0,
+ });
+ });
+ Fantom.runWorkLoop();
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "ScrollView", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: "child A"}',
+ 'Create {type: "View", nativeID: "child B"}',
+ 'Create {type: "View", nativeID: "child BA"}',
+ 'Create {type: "View", nativeID: "child BB"}',
+ 'Insert {type: "View", parentNativeID: "child B", index: 0, nativeID: "child BA"}',
+ 'Insert {type: "View", parentNativeID: "child B", index: 1, nativeID: "child BB"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "child A"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 1, nativeID: "child B"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: (N/A)}',
+ ]);
+});
+
+test('deep nesting', () => {
+ const root = Fantom.createRoot({viewportHeight: 100, viewportWidth: 100});
+ let maybeNode;
+
+ Fantom.runTask(() => {
+ root.render(
+ {
+ maybeNode = node;
+ }}>
+
+
+
+
+
+
+
+ ,
+ );
+ });
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "RootView", nativeID: (root)}',
+ 'Create {type: "ScrollView", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: "element A"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "element A"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: (N/A)}',
+ 'Insert {type: "ScrollView", parentNativeID: (root), index: 0, nativeID: (N/A)}',
+ ]);
+
+ const element = ensureInstance(maybeNode, ReactNativeElement);
+
+ Fantom.runOnUIThread(() => {
+ Fantom.scrollTo(element, {
+ x: 0,
+ y: 40,
+ });
+ });
+ Fantom.runWorkLoop();
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "ScrollView", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: "element B"}',
+ 'Create {type: "View", nativeID: "child BA"}',
+ 'Create {type: "View", nativeID: "child BAA"}',
+ 'Insert {type: "View", parentNativeID: "child BA", index: 0, nativeID: "child BAA"}',
+ 'Insert {type: "View", parentNativeID: "element B", index: 0, nativeID: "child BA"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 1, nativeID: "element B"}',
+ ]);
+
+ Fantom.runOnUIThread(() => {
+ Fantom.scrollTo(element, {
+ x: 0,
+ y: 150,
+ });
+ });
+ Fantom.runWorkLoop();
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "ScrollView", nativeID: (N/A)}',
+ 'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: "element A"}',
+ 'Delete {type: "View", nativeID: "element A"}',
+ 'Create {type: "View", nativeID: "child BAB"}',
+ 'Insert {type: "View", parentNativeID: "child BA", index: 1, nativeID: "child BAB"}',
+ ]);
+});
+
+test('adding new item into area that is not culled', () => {
+ const root = Fantom.createRoot({viewportHeight: 100, viewportWidth: 100});
+
+ Fantom.runTask(() => {
+ root.render(
+
+
+ ,
+ );
+ });
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "RootView", nativeID: (root)}',
+ 'Create {type: "ScrollView", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: "element A"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "element A"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: (N/A)}',
+ 'Insert {type: "ScrollView", parentNativeID: (root), index: 0, nativeID: (N/A)}',
+ ]);
+
+ Fantom.runTask(() => {
+ root.render(
+
+
+
+
+ ,
+ );
+ });
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Create {type: "View", nativeID: "child AA"}',
+ 'Insert {type: "View", parentNativeID: "element A", index: 0, nativeID: "child AA"}',
+ ]);
+});
+
+test('adding new item into area that is culled', () => {
+ const root = Fantom.createRoot({viewportHeight: 100, viewportWidth: 100});
+
+ Fantom.runTask(() => {
+ root.render(
+
+
+ ,
+ );
+ });
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "RootView", nativeID: (root)}',
+ 'Create {type: "ScrollView", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: "element B"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "element B"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: (N/A)}',
+ 'Insert {type: "ScrollView", parentNativeID: (root), index: 0, nativeID: (N/A)}',
+ ]);
+
+ Fantom.runTask(() => {
+ root.render(
+
+
+
+ ,
+ );
+ });
+
+ // element B is updated but it should be inconsequential.
+ // Differentiator generates an update for it because Yoga cloned
+ // shadow node backing element B.
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "View", nativeID: "element B"}',
+ ]);
+});
+
+test('initial render', () => {
+ let maybeNode;
+ const root = Fantom.createRoot({viewportHeight: 100, viewportWidth: 100});
+
+ Fantom.runTask(() => {
+ root.render(
+ {
+ maybeNode = node;
+ }}
+ style={{height: 100, width: 100}}>
+
+
+
+
+
+ ,
+ );
+ });
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "RootView", nativeID: (root)}',
+ 'Create {type: "ScrollView", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: "element A"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "element A"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: (N/A)}',
+ 'Insert {type: "ScrollView", parentNativeID: (root), index: 0, nativeID: (N/A)}',
+ ]);
+
+ const element = ensureInstance(maybeNode, ReactNativeElement);
+
+ Fantom.runOnUIThread(() => {
+ Fantom.scrollTo(element, {
+ x: 0,
+ y: 100,
+ });
+ });
+ Fantom.runWorkLoop();
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "ScrollView", nativeID: (N/A)}',
+ 'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: "element A"}',
+ 'Delete {type: "View", nativeID: "element A"}',
+ 'Create {type: "View", nativeID: "element B"}',
+ 'Create {type: "View", nativeID: "child BA"}',
+ 'Create {type: "View", nativeID: "child BB"}',
+ 'Insert {type: "View", parentNativeID: "element B", index: 0, nativeID: "child BA"}',
+ 'Insert {type: "View", parentNativeID: "element B", index: 1, nativeID: "child BB"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "element B"}',
+ ]);
+});
+
+test('unmounting culled elements', () => {
+ const root = Fantom.createRoot({viewportWidth: 100, viewportHeight: 100});
+
+ Fantom.runTask(() => {
+ root.render(
+
+
+ ,
+ );
+ });
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "RootView", nativeID: (root)}',
+ 'Create {type: "ScrollView", nativeID: (N/A)}',
+ 'Insert {type: "ScrollView", parentNativeID: (root), index: 0, nativeID: (N/A)}',
+ ]);
+
+ Fantom.runTask(() => {
+ root.render(<>>);
+ });
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Remove {type: "ScrollView", parentNativeID: (root), index: 0, nativeID: (N/A)}',
+ 'Delete {type: "ScrollView", nativeID: (N/A)}',
+ ]);
+});
+
+// TODO: only elements in ScrollView are culled.
+test('basic culling smaller ScrollView', () => {
+ let maybeNode;
+ const root = Fantom.createRoot({viewportWidth: 100, viewportHeight: 100});
+
+ Fantom.runTask(() => {
+ root.render(
+ {
+ maybeNode = node;
+ }}
+ style={{height: 50, width: 50, marginTop: 25}}>
+
+ ,
+ );
+ });
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "RootView", nativeID: (root)}',
+ 'Create {type: "ScrollView", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: "element 1"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "element 1"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: (N/A)}',
+ 'Insert {type: "ScrollView", parentNativeID: (root), index: 0, nativeID: (N/A)}',
+ ]);
+
+ const element = ensureInstance(maybeNode, ReactNativeElement);
+
+ Fantom.runOnUIThread(() => {
+ Fantom.scrollTo(element, {
+ x: 0,
+ y: 11,
+ });
+ });
+ Fantom.runWorkLoop();
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: "element 1"}',
+ 'Delete {type: "View", nativeID: "element 1"}',
+ 'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: (N/A)}',
+ 'Delete {type: "View", nativeID: (N/A)}',
+ 'Update {type: "ScrollView", nativeID: (N/A)}',
+ ]);
+});
+
+test('views are not culled when outside of viewport', () => {
+ const root = Fantom.createRoot({viewportWidth: 100, viewportHeight: 100});
+
+ Fantom.runTask(() => {
+ root.render(
+ ,
+ );
+ });
+
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Update {type: "RootView", nativeID: (root)}',
+ 'Create {type: "View", nativeID: "child"}',
+ 'Insert {type: "View", parentNativeID: (root), index: 0, nativeID: "child"}',
+ ]);
+});
diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm
index e1d731ae17ee07..7552cee49fac3e 100644
--- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm
+++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm
@@ -64,6 +64,7 @@ - (instancetype)initWithFrame:(CGRect)frame
_reactSubviews = [NSMutableArray new];
self.multipleTouchEnabled = YES;
_useCustomContainerView = NO;
+ _removeClippedSubviews = NO;
}
return self;
}
@@ -229,10 +230,13 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
needsInvalidateLayer = YES;
}
- if (oldViewProps.removeClippedSubviews != newViewProps.removeClippedSubviews) {
- _removeClippedSubviews = newViewProps.removeClippedSubviews;
- if (_removeClippedSubviews && self.currentContainerView.subviews.count > 0) {
- _reactSubviews = [NSMutableArray arrayWithArray:self.currentContainerView.subviews];
+ // Disable `removeClippedSubviews` when Fabric View Culling is enabled.
+ if (!ReactNativeFeatureFlags::enableViewCulling()) {
+ if (oldViewProps.removeClippedSubviews != newViewProps.removeClippedSubviews) {
+ _removeClippedSubviews = newViewProps.removeClippedSubviews;
+ if (_removeClippedSubviews && self.currentContainerView.subviews.count > 0) {
+ _reactSubviews = [NSMutableArray arrayWithArray:self.currentContainerView.subviews];
+ }
}
}
diff --git a/packages/react-native/ReactCommon/React-Fabric.podspec b/packages/react-native/ReactCommon/React-Fabric.podspec
index 08b5134becc658..eff21eaeee78e8 100644
--- a/packages/react-native/ReactCommon/React-Fabric.podspec
+++ b/packages/react-native/ReactCommon/React-Fabric.podspec
@@ -150,6 +150,14 @@ Pod::Spec.new do |s|
sss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/Headers/Private/Yoga\"" }
end
+ ss.subspec "scrollview" do |sss|
+ sss.dependency folly_dep_name, folly_version
+ sss.compiler_flags = folly_compiler_flags
+ sss.source_files = "react/renderer/components/scrollview/*.{m,mm,cpp,h}"
+ sss.header_dir = "react/renderer/components/scrollview"
+ ss.exclude_files = "react/renderer/components/scrollview/tests"
+ end
+
ss.subspec "legacyviewmanagerinterop" do |sss|
sss.dependency folly_dep_name, folly_version
sss.compiler_flags = folly_compiler_flags
diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/Point.h b/packages/react-native/ReactCommon/react/renderer/graphics/Point.h
index 34ffe2ad54daf5..fbd0cf94866bd5 100644
--- a/packages/react-native/ReactCommon/react/renderer/graphics/Point.h
+++ b/packages/react-native/ReactCommon/react/renderer/graphics/Point.h
@@ -33,6 +33,13 @@ struct Point {
return *this;
}
+ Point operator-() noexcept {
+ return {
+ .x = -x,
+ .y = -y,
+ };
+ }
+
Point& operator*=(const Point& point) noexcept {
x *= point.x;
y *= point.y;
diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp
index f38e0fa7109a34..379f15eef562ef 100644
--- a/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp
+++ b/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp
@@ -10,6 +10,7 @@
#include
#include
#include
+#include
#include
#include
@@ -233,6 +234,23 @@ static bool shouldFirstPairComesBeforeSecondOne(
return lhs->shadowNode->getOrderIndex() < rhs->shadowNode->getOrderIndex();
}
+static Rect adjustCullingFrameIfNeeded(
+ Rect cullingFrame,
+ const ShadowViewNodePair& pair) {
+ if (ReactNativeFeatureFlags::enableViewCulling()) {
+ if (auto scrollViewShadowNode =
+ dynamic_cast(pair.shadowNode)) {
+ cullingFrame.origin = -scrollViewShadowNode->getContentOriginOffset(
+ /* includeTransform */ true);
+ cullingFrame.size = scrollViewShadowNode->getLayoutMetrics().frame.size;
+ } else {
+ cullingFrame.origin -= pair.shadowView.layoutMetrics.frame.origin;
+ }
+ }
+
+ return cullingFrame;
+}
+
/*
* Reorders pairs in-place based on `orderIndex` using a stable sort algorithm.
*/
@@ -263,10 +281,10 @@ static void sliceChildShadowNodeViewPairsRecursively(
size_t& startOfStaticIndex,
ViewNodePairScope& scope,
Point layoutOffset,
- const ShadowNode& shadowNode) {
+ const ShadowNode& shadowNode,
+ Rect cullingFrame) {
for (const auto& sharedChildShadowNode : shadowNode.getChildren()) {
auto& childShadowNode = *sharedChildShadowNode;
-
#ifndef ANDROID
// T153547836: Disabled on Android because the mounting infrastructure
// is not fully ready yet.
@@ -274,9 +292,28 @@ static void sliceChildShadowNodeViewPairsRecursively(
continue;
}
#endif
-
auto shadowView = ShadowView(childShadowNode);
+
+ if (ReactNativeFeatureFlags::enableViewCulling()) {
+ auto isCullingFrameValid =
+ cullingFrame.size.width != 0 && cullingFrame.size.height != 0;
+ if (isCullingFrameValid &&
+ shadowView.layoutMetrics != EmptyLayoutMetrics) {
+ auto doesIntersect =
+ Rect::intersect(
+ cullingFrame,
+ shadowView.layoutMetrics.getOverflowInsetFrame()) != Rect{};
+ if (!doesIntersect) {
+ continue; // Culling.
+ }
+ }
+ }
+
auto origin = layoutOffset;
+ auto cullingFrameCopy = adjustCullingFrameIfNeeded(
+ cullingFrame,
+ {.shadowView = shadowView, .shadowNode = &childShadowNode});
+
if (shadowView.layoutMetrics != EmptyLayoutMetrics) {
origin += shadowView.layoutMetrics.frame.origin;
shadowView.layoutMetrics.frame.origin += layoutOffset;
@@ -317,14 +354,24 @@ static void sliceChildShadowNodeViewPairsRecursively(
startOfStaticIndex++;
if (areChildrenFlattened) {
sliceChildShadowNodeViewPairsRecursively(
- pairList, startOfStaticIndex, scope, origin, childShadowNode);
+ pairList,
+ startOfStaticIndex,
+ scope,
+ origin,
+ childShadowNode,
+ cullingFrameCopy);
}
} else {
pairList.push_back(&scope.back());
if (areChildrenFlattened) {
size_t pairListSize = pairList.size();
sliceChildShadowNodeViewPairsRecursively(
- pairList, pairListSize, scope, origin, childShadowNode);
+ pairList,
+ pairListSize,
+ scope,
+ origin,
+ childShadowNode,
+ cullingFrameCopy);
}
}
}
@@ -334,7 +381,8 @@ std::vector sliceChildShadowNodeViewPairs(
const ShadowViewNodePair& shadowNodePair,
ViewNodePairScope& scope,
bool allowFlattened,
- Point layoutOffset) {
+ Point layoutOffset,
+ Rect cullingFrame) {
const auto& shadowNode = *shadowNodePair.shadowNode;
auto pairList = std::vector{};
@@ -346,7 +394,12 @@ std::vector sliceChildShadowNodeViewPairs(
size_t startOfStaticIndex = 0;
sliceChildShadowNodeViewPairsRecursively(
- pairList, startOfStaticIndex, scope, layoutOffset, shadowNode);
+ pairList,
+ startOfStaticIndex,
+ scope,
+ layoutOffset,
+ shadowNode,
+ cullingFrame);
// Sorting pairs based on `orderIndex` if needed.
reorderInPlaceIfNeeded(pairList);
@@ -369,12 +422,14 @@ static std::vector
sliceChildShadowNodeViewPairsFromViewNodePair(
const ShadowViewNodePair& shadowViewNodePair,
ViewNodePairScope& scope,
- bool allowFlattened = false) {
+ bool allowFlattened,
+ Rect cullingFrame) {
return sliceChildShadowNodeViewPairs(
shadowViewNodePair,
scope,
allowFlattened,
- shadowViewNodePair.contextOrigin);
+ shadowViewNodePair.contextOrigin,
+ cullingFrame);
}
/*
@@ -409,7 +464,9 @@ static void calculateShadowViewMutations(
ShadowViewMutation::List& mutations,
Tag parentTag,
std::vector&& oldChildPairs,
- std::vector&& newChildPairs);
+ std::vector&& newChildPairs,
+ Rect oldCullingFrame = {},
+ Rect newCullingFrame = {});
struct OrderedMutationInstructionContainer {
ShadowViewMutation::List createMutations{};
@@ -428,7 +485,9 @@ static void updateMatchedPairSubtrees(
std::vector& oldChildPairs,
Tag parentTag,
const ShadowViewNodePair& oldPair,
- const ShadowViewNodePair& newPair);
+ const ShadowViewNodePair& newPair,
+ Rect oldCullingFrame,
+ Rect newCullingFrame);
static void updateMatchedPair(
OrderedMutationInstructionContainer& mutationContainer,
@@ -446,8 +505,10 @@ static void calculateShadowViewMutationsFlattener(
TinyMap& unvisitedOtherNodes,
const ShadowViewNodePair& node,
Tag parentTagForUpdate,
- TinyMap* parentSubVisitedOtherNewNodes = nullptr,
- TinyMap* parentSubVisitedOtherOldNodes = nullptr);
+ TinyMap* parentSubVisitedOtherNewNodes,
+ TinyMap* parentSubVisitedOtherOldNodes,
+ Rect oldCullingFrame,
+ Rect newCullingFrame);
/**
* Updates the subtrees of any matched ShadowViewNodePair. This handles
@@ -464,7 +525,9 @@ static void updateMatchedPairSubtrees(
std::vector& oldChildPairs,
Tag parentTag,
const ShadowViewNodePair& oldPair,
- const ShadowViewNodePair& newPair) {
+ const ShadowViewNodePair& newPair,
+ Rect oldCullingFrame,
+ Rect newCullingFrame) {
// Are we flattening or unflattening either one? If node was
// flattened in both trees, there's no change, just continue.
if (oldPair.flattened && newPair.flattened) {
@@ -493,7 +556,11 @@ static void updateMatchedPairSubtrees(
parentTag,
newRemainingPairs,
oldPair,
- oldPair.shadowView.tag);
+ oldPair.shadowView.tag,
+ nullptr,
+ nullptr,
+ oldCullingFrame,
+ newCullingFrame);
}
// Unflattening
else {
@@ -504,8 +571,8 @@ static void updateMatchedPairSubtrees(
// relative order. The reason for this is because of flattening
// + zIndex: the children could be listed before the parent,
// interwoven with children from other nodes, etc.
- auto oldFlattenedNodes =
- sliceChildShadowNodeViewPairsFromViewNodePair(oldPair, scope, true);
+ auto oldFlattenedNodes = sliceChildShadowNodeViewPairsFromViewNodePair(
+ oldPair, scope, true, oldCullingFrame);
for (size_t i = 0, j = 0;
i < oldChildPairs.size() && j < oldFlattenedNodes.size();
i++) {
@@ -524,7 +591,11 @@ static void updateMatchedPairSubtrees(
parentTag,
unvisitedOldChildPairs,
newPair,
- parentTag);
+ parentTag,
+ nullptr,
+ nullptr,
+ oldCullingFrame,
+ newCullingFrame);
// If old nodes were not visited, we know that we can delete
// them now. They will be removed from the hierarchy by the
@@ -551,13 +622,18 @@ static void updateMatchedPairSubtrees(
// Update subtrees if View is not flattened, and if node addresses
// are not equal
- if (oldPair.shadowNode != newPair.shadowNode) {
+ if (oldPair.shadowNode != newPair.shadowNode ||
+ oldCullingFrame != newCullingFrame) {
+ oldCullingFrame = adjustCullingFrameIfNeeded(oldCullingFrame, oldPair);
+ newCullingFrame = adjustCullingFrameIfNeeded(newCullingFrame, newPair);
+
ViewNodePairScope innerScope{};
- auto oldGrandChildPairs =
- sliceChildShadowNodeViewPairsFromViewNodePair(oldPair, innerScope);
- auto newGrandChildPairs =
- sliceChildShadowNodeViewPairsFromViewNodePair(newPair, innerScope);
+ auto oldGrandChildPairs = sliceChildShadowNodeViewPairsFromViewNodePair(
+ oldPair, innerScope, false, oldCullingFrame);
+ auto newGrandChildPairs = sliceChildShadowNodeViewPairsFromViewNodePair(
+ newPair, innerScope, false, newCullingFrame);
const size_t newGrandChildPairsSize = newGrandChildPairs.size();
+
calculateShadowViewMutations(
innerScope,
*(newGrandChildPairsSize != 0u
@@ -565,7 +641,9 @@ static void updateMatchedPairSubtrees(
: &mutationContainer.destructiveDownwardMutations),
oldPair.shadowView.tag,
std::move(oldGrandChildPairs),
- std::move(newGrandChildPairs));
+ std::move(newGrandChildPairs),
+ oldCullingFrame,
+ newCullingFrame);
}
}
@@ -679,10 +757,13 @@ static void calculateShadowViewMutationsFlattener(
const ShadowViewNodePair& node,
Tag parentTagForUpdate,
TinyMap* parentSubVisitedOtherNewNodes,
- TinyMap* parentSubVisitedOtherOldNodes) {
+ TinyMap* parentSubVisitedOtherOldNodes,
+ Rect oldCullingFrame,
+ Rect newCullingFrame) {
// Step 1: iterate through entire tree
std::vector treeChildren =
- sliceChildShadowNodeViewPairsFromViewNodePair(node, scope);
+ sliceChildShadowNodeViewPairsFromViewNodePair(
+ node, scope, false, newCullingFrame);
DEBUG_LOGS({
LOG(ERROR) << "Differ Flattener: "
@@ -885,9 +966,11 @@ static void calculateShadowViewMutationsFlattener(
mutationContainer.downwardMutations,
newTreeNodePair.shadowView.tag,
sliceChildShadowNodeViewPairsFromViewNodePair(
- oldTreeNodePair, innerScope),
+ oldTreeNodePair, innerScope, false, oldCullingFrame),
sliceChildShadowNodeViewPairsFromViewNodePair(
- newTreeNodePair, innerScope));
+ newTreeNodePair, innerScope, false, newCullingFrame),
+ oldCullingFrame,
+ newCullingFrame);
}
} else if (oldTreeNodePair.flattened != newTreeNodePair.flattened) {
// We need to handle one of the children being flattened or
@@ -912,14 +995,18 @@ static void calculateShadowViewMutationsFlattener(
? oldTreeNodePair.shadowView.tag
: parentTag),
subVisitedNewMap,
- subVisitedOldMap);
+ subVisitedOldMap,
+ oldCullingFrame,
+ newCullingFrame);
} else {
// Get flattened nodes from either new or old tree
auto flattenedNodes = sliceChildShadowNodeViewPairsFromViewNodePair(
(childReparentMode == ReparentMode::Flatten ? newTreeNodePair
: oldTreeNodePair),
scope,
- true);
+ true,
+ childReparentMode == ReparentMode::Flatten ? newCullingFrame
+ : oldCullingFrame);
// Construct unvisited nodes map
auto unvisitedRecursiveChildPairs =
TinyMap{};
@@ -956,7 +1043,9 @@ static void calculateShadowViewMutationsFlattener(
? oldTreeNodePair.shadowView.tag
: parentTag),
subVisitedNewMap,
- subVisitedOldMap);
+ subVisitedOldMap,
+ oldCullingFrame,
+ newCullingFrame);
}
// Flatten parent, unflatten child
else {
@@ -974,7 +1063,9 @@ static void calculateShadowViewMutationsFlattener(
? oldTreeNodePair.shadowView.tag
: parentTag),
subVisitedNewMap,
- subVisitedOldMap);
+ subVisitedOldMap,
+ oldCullingFrame,
+ newCullingFrame);
// If old nodes were not visited, we know that we can delete them
// now. They will be removed from the hierarchy by the outermost
@@ -1070,8 +1161,10 @@ static void calculateShadowViewMutationsFlattener(
mutationContainer.destructiveDownwardMutations,
treeChildPair.shadowView.tag,
sliceChildShadowNodeViewPairsFromViewNodePair(
- treeChildPair, innerScope),
- {});
+ treeChildPair, innerScope, false, newCullingFrame),
+ {},
+ oldCullingFrame,
+ newCullingFrame);
}
} else {
mutationContainer.createMutations.push_back(
@@ -1085,7 +1178,9 @@ static void calculateShadowViewMutationsFlattener(
treeChildPair.shadowView.tag,
{},
sliceChildShadowNodeViewPairsFromViewNodePair(
- treeChildPair, innerScope));
+ treeChildPair, innerScope, false, newCullingFrame),
+ oldCullingFrame,
+ newCullingFrame);
}
}
}
@@ -1096,7 +1191,9 @@ static void calculateShadowViewMutations(
ShadowViewMutation::List& mutations,
Tag parentTag,
std::vector&& oldChildPairs,
- std::vector&& newChildPairs) {
+ std::vector&& newChildPairs,
+ Rect oldCullingFrame,
+ Rect newCullingFrame) {
if (oldChildPairs.empty() && newChildPairs.empty()) {
return;
}
@@ -1152,13 +1249,21 @@ static void calculateShadowViewMutations(
// Recursively update tree if ShadowNode pointers are not equal
if (!oldChildPair.flattened &&
- oldChildPair.shadowNode != newChildPair.shadowNode) {
+ (oldChildPair.shadowNode != newChildPair.shadowNode ||
+ oldCullingFrame != newCullingFrame)) {
+ auto oldCullingFrameCopy =
+ adjustCullingFrameIfNeeded(oldCullingFrame, oldChildPair);
+ auto newCullingFrameCopy =
+ adjustCullingFrameIfNeeded(newCullingFrame, newChildPair);
+
ViewNodePairScope innerScope{};
auto oldGrandChildPairs = sliceChildShadowNodeViewPairsFromViewNodePair(
- oldChildPair, innerScope);
+ oldChildPair, innerScope, false, oldCullingFrameCopy);
auto newGrandChildPairs = sliceChildShadowNodeViewPairsFromViewNodePair(
- newChildPair, innerScope);
+ newChildPair, innerScope, false, newCullingFrameCopy);
+
const size_t newGrandChildPairsSize = newGrandChildPairs.size();
+
calculateShadowViewMutations(
innerScope,
*(newGrandChildPairsSize != 0u
@@ -1166,7 +1271,9 @@ static void calculateShadowViewMutations(
: &mutationContainer.destructiveDownwardMutations),
oldChildPair.shadowView.tag,
std::move(oldGrandChildPairs),
- std::move(newGrandChildPairs));
+ std::move(newGrandChildPairs),
+ oldCullingFrameCopy,
+ newCullingFrameCopy);
}
}
@@ -1194,6 +1301,8 @@ static void calculateShadowViewMutations(
parentTag,
oldChildPair.shadowView,
static_cast(oldChildPair.mountIndex)));
+ auto oldCullingFrameCopy =
+ adjustCullingFrameIfNeeded(oldCullingFrame, oldChildPair);
// We also have to call the algorithm recursively to clean up the entire
// subtree starting from the removed view.
@@ -1203,8 +1312,10 @@ static void calculateShadowViewMutations(
mutationContainer.destructiveDownwardMutations,
oldChildPair.shadowView.tag,
sliceChildShadowNodeViewPairsFromViewNodePair(
- oldChildPair, innerScope),
- {});
+ oldChildPair, innerScope, false, oldCullingFrameCopy),
+ {},
+ oldCullingFrameCopy,
+ newCullingFrame);
}
} else if (index == oldChildPairs.size()) {
// If we don't have any more existing children we can choose a fast path
@@ -1228,6 +1339,8 @@ static void calculateShadowViewMutations(
static_cast(newChildPair.mountIndex)));
mutationContainer.createMutations.push_back(
ShadowViewMutation::CreateMutation(newChildPair.shadowView));
+ auto newCullingFrameCopy =
+ adjustCullingFrameIfNeeded(newCullingFrame, newChildPair);
ViewNodePairScope innerScope{};
calculateShadowViewMutations(
@@ -1236,7 +1349,9 @@ static void calculateShadowViewMutations(
newChildPair.shadowView.tag,
{},
sliceChildShadowNodeViewPairsFromViewNodePair(
- newChildPair, innerScope));
+ newChildPair, innerScope, false, newCullingFrameCopy),
+ oldCullingFrame,
+ newCullingFrameCopy);
}
} else {
// Collect map of tags in the new list
@@ -1290,7 +1405,9 @@ static void calculateShadowViewMutations(
oldChildPairs,
parentTag,
oldChildPair,
- newChildPair);
+ newChildPair,
+ oldCullingFrame,
+ newCullingFrame);
newIndex++;
oldIndex++;
@@ -1334,7 +1451,9 @@ static void calculateShadowViewMutations(
oldChildPairs,
parentTag,
oldChildPair,
- newChildPair);
+ newChildPair,
+ oldCullingFrame,
+ newCullingFrame);
newInsertedPairs.erase(insertedIt);
oldIndex++;
@@ -1456,17 +1575,23 @@ static void calculateShadowViewMutations(
if (!oldChildPair.inOtherTree() && oldChildPair.isConcreteView) {
mutationContainer.deleteMutations.push_back(
ShadowViewMutation::DeleteMutation(oldChildPair.shadowView));
+ auto oldCullingFrameCopy =
+ adjustCullingFrameIfNeeded(oldCullingFrame, oldChildPair);
// We also have to call the algorithm recursively to clean up the
// entire subtree starting from the removed view.
ViewNodePairScope innerScope{};
+
+ auto newGrandChildPairs = sliceChildShadowNodeViewPairsFromViewNodePair(
+ oldChildPair, innerScope, false, oldCullingFrameCopy);
calculateShadowViewMutations(
innerScope,
mutationContainer.destructiveDownwardMutations,
oldChildPair.shadowView.tag,
- sliceChildShadowNodeViewPairsFromViewNodePair(
- oldChildPair, innerScope),
- {});
+ std::move(newGrandChildPairs),
+ {},
+ oldCullingFrameCopy,
+ newCullingFrame);
}
}
@@ -1501,14 +1626,20 @@ static void calculateShadowViewMutations(
mutationContainer.createMutations.push_back(
ShadowViewMutation::CreateMutation(newChildPair.shadowView));
+ auto newCullingFrameCopy =
+ adjustCullingFrameIfNeeded(newCullingFrame, newChildPair);
+
ViewNodePairScope innerScope{};
+
calculateShadowViewMutations(
innerScope,
mutationContainer.downwardMutations,
newChildPair.shadowView.tag,
{},
sliceChildShadowNodeViewPairsFromViewNodePair(
- newChildPair, innerScope));
+ newChildPair, innerScope, false, newCullingFrameCopy),
+ oldCullingFrame,
+ newCullingFrameCopy);
}
}
@@ -1567,16 +1698,22 @@ ShadowViewMutation::List calculateShadowViewMutations(
oldRootShadowView, newRootShadowView, {}));
}
+ auto sliceOne = sliceChildShadowNodeViewPairs(
+ ShadowViewNodePair{.shadowNode = &oldRootShadowNode},
+ viewNodePairScope,
+ false,
+ {});
+ auto sliceTwo = sliceChildShadowNodeViewPairs(
+ ShadowViewNodePair{.shadowNode = &newRootShadowNode},
+ viewNodePairScope,
+ false,
+ {});
calculateShadowViewMutations(
innerViewNodePairScope,
mutations,
oldRootShadowNode.getTag(),
- sliceChildShadowNodeViewPairs(
- ShadowViewNodePair{.shadowNode = &oldRootShadowNode},
- viewNodePairScope),
- sliceChildShadowNodeViewPairs(
- ShadowViewNodePair{.shadowNode = &newRootShadowNode},
- viewNodePairScope));
+ std::move(sliceOne),
+ std::move(sliceTwo));
DEBUG_LOGS({
LOG(ERROR) << "Differ Completed: " << mutations.size() << " mutations";
diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.h b/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.h
index cf8dcf8cc0d975..e0d304b9cec915 100644
--- a/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.h
+++ b/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.h
@@ -95,7 +95,8 @@ ShadowViewMutation::List calculateShadowViewMutations(
std::vector sliceChildShadowNodeViewPairs(
const ShadowViewNodePair& shadowNodePair,
ViewNodePairScope& viewNodePairScope,
- bool allowFlattened = false,
- Point layoutOffset = {0, 0});
+ bool allowFlattened,
+ Point layoutOffset,
+ Rect cullingFrame = {});
} // namespace facebook::react
diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/stubs/stubs.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/stubs/stubs.cpp
index 9c3cc7e6882cfd..7a79245e2f820a 100644
--- a/packages/react-native/ReactCommon/react/renderer/mounting/stubs/stubs.cpp
+++ b/packages/react-native/ReactCommon/react/renderer/mounting/stubs/stubs.cpp
@@ -60,7 +60,7 @@ static void calculateShadowViewMutationsForNewTree(
static_cast(newChildPair->mountIndex)));
auto newGrandChildPairs =
- sliceChildShadowNodeViewPairs(*newChildPair, scope);
+ sliceChildShadowNodeViewPairs(*newChildPair, scope, false, {}, {});
calculateShadowViewMutationsForNewTree(
mutations, scope, newChildPair->shadowView, newGrandChildPairs);
@@ -78,7 +78,7 @@ StubViewTree buildStubViewTreeWithoutUsingDifferentiator(
mutations,
scope,
ShadowView(rootShadowNode),
- sliceChildShadowNodeViewPairs(rootShadowNodePair, scope));
+ sliceChildShadowNodeViewPairs(rootShadowNodePair, scope, false, {}, {}));
auto emptyRootShadowNode = rootShadowNode.clone(ShadowNodeFragment{
ShadowNodeFragment::propsPlaceholder(),