Skip to content

Commit da30672

Browse files
committed
Add ref to Fragment (#32465)
*This API is experimental and subject to change or removal.* This PR is an alternative to #32421 based on feedback: #32421 (review) . The difference here is that we traverse from the Fragment's fiber at operation time instead of keeping a set of children on the `FragmentInstance`. We still need to handle newly added or removed child nodes to apply event listeners and observers, so we treat those updates as effects. **Fragment Refs** This PR extends React's Fragment component to accept a `ref` prop. The Fragment's ref will attach to a custom host instance, which will provide an Element-like API for working with the Fragment's host parent and host children. Here I've implemented `addEventListener`, `removeEventListener`, and `focus` to get started but we'll be iterating on this by adding additional APIs in future PRs. This sets up the mechanism to attach refs and perform operations on children. The FragmentInstance is implemented in `react-dom` here but is planned for Fabric as well. The API works by targeting the first level of host children and proxying Element-like APIs to allow developers to manage groups of elements or elements that cannot be easily accessed such as from a third-party library or deep in a tree of Functional Component wrappers. ```javascript import {Fragment, useRef} from 'react'; const fragmentRef = useRef(null); <Fragment ref={fragmentRef}> <div id="A" /> <Wrapper> <div id="B"> <div id="C" /> </div> </Wrapper> <div id="D" /> </Fragment> ``` In this case, calling `fragmentRef.current.addEventListener()` would apply an event listener to `A`, `B`, and `D`. `C` is skipped because it is nested under the first level of Host Component. If another Host Component was appended as a sibling to `A`, `B`, or `D`, the event listener would be applied to that element as well and any other APIs would also affect the newly added child. This is an implementation of the basic feature as a starting point for feedback and further iteration. DiffTrain build for [6aa8254](6aa8254)
1 parent 11e1edc commit da30672

35 files changed

+6593
-2866
lines changed

compiled/facebook-www/REVISION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
ca8f91f6f6b1b31023eee06c1e2a827ee178b68b
1+
6aa8254bb7353fe3096289edc669cf168e9fd190
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
ca8f91f6f6b1b31023eee06c1e2a827ee178b68b
1+
6aa8254bb7353fe3096289edc669cf168e9fd190

compiled/facebook-www/React-dev.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1532,7 +1532,7 @@ __DEV__ &&
15321532
exports.useTransition = function () {
15331533
return resolveDispatcher().useTransition();
15341534
};
1535-
exports.version = "19.1.0-www-classic-ca8f91f6-20250311";
1535+
exports.version = "19.1.0-www-classic-6aa8254b-20250312";
15361536
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
15371537
"function" ===
15381538
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/React-dev.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1532,7 +1532,7 @@ __DEV__ &&
15321532
exports.useTransition = function () {
15331533
return resolveDispatcher().useTransition();
15341534
};
1535-
exports.version = "19.1.0-www-modern-ca8f91f6-20250311";
1535+
exports.version = "19.1.0-www-modern-6aa8254b-20250312";
15361536
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
15371537
"function" ===
15381538
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/React-prod.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -641,4 +641,4 @@ exports.useSyncExternalStore = function (
641641
exports.useTransition = function () {
642642
return ReactSharedInternals.H.useTransition();
643643
};
644-
exports.version = "19.1.0-www-classic-ca8f91f6-20250311";
644+
exports.version = "19.1.0-www-classic-6aa8254b-20250312";

compiled/facebook-www/React-prod.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -641,4 +641,4 @@ exports.useSyncExternalStore = function (
641641
exports.useTransition = function () {
642642
return ReactSharedInternals.H.useTransition();
643643
};
644-
exports.version = "19.1.0-www-modern-ca8f91f6-20250311";
644+
exports.version = "19.1.0-www-modern-6aa8254b-20250312";

compiled/facebook-www/React-profiling.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -645,7 +645,7 @@ exports.useSyncExternalStore = function (
645645
exports.useTransition = function () {
646646
return ReactSharedInternals.H.useTransition();
647647
};
648-
exports.version = "19.1.0-www-classic-ca8f91f6-20250311";
648+
exports.version = "19.1.0-www-classic-6aa8254b-20250312";
649649
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
650650
"function" ===
651651
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/React-profiling.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -645,7 +645,7 @@ exports.useSyncExternalStore = function (
645645
exports.useTransition = function () {
646646
return ReactSharedInternals.H.useTransition();
647647
};
648-
exports.version = "19.1.0-www-modern-ca8f91f6-20250311";
648+
exports.version = "19.1.0-www-modern-6aa8254b-20250312";
649649
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
650650
"function" ===
651651
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/ReactART-dev.classic.js

Lines changed: 104 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5112,18 +5112,27 @@ __DEV__ &&
51125112
function validateFragmentProps(element, fiber, returnFiber) {
51135113
for (var keys = Object.keys(element.props), i = 0; i < keys.length; i++) {
51145114
var key = keys[i];
5115-
if ("children" !== key && "key" !== key) {
5115+
if (
5116+
"children" !== key &&
5117+
"key" !== key &&
5118+
(enableFragmentRefs ? "ref" !== key : 1)
5119+
) {
51165120
null === fiber &&
51175121
((fiber = createFiberFromElement(element, returnFiber.mode, 0)),
51185122
(fiber._debugInfo = currentDebugInfo),
51195123
(fiber.return = returnFiber));
51205124
runWithFiberInDEV(
51215125
fiber,
51225126
function (erroredKey) {
5123-
console.error(
5124-
"Invalid prop `%s` supplied to `React.Fragment`. React.Fragment can only have `key` and `children` props.",
5125-
erroredKey
5126-
);
5127+
enableFragmentRefs
5128+
? console.error(
5129+
"Invalid prop `%s` supplied to `React.Fragment`. React.Fragment can only have `key`, `ref`, and `children` props.",
5130+
erroredKey
5131+
)
5132+
: console.error(
5133+
"Invalid prop `%s` supplied to `React.Fragment`. React.Fragment can only have `key` and `children` props.",
5134+
erroredKey
5135+
);
51275136
},
51285137
key
51295138
);
@@ -5276,6 +5285,7 @@ __DEV__ &&
52765285
lanes,
52775286
element.key
52785287
)),
5288+
enableFragmentRefs && coerceRef(current, element),
52795289
validateFragmentProps(element, current, returnFiber),
52805290
current
52815291
);
@@ -5868,6 +5878,7 @@ __DEV__ &&
58685878
null !== newChild &&
58695879
newChild.type === REACT_FRAGMENT_TYPE &&
58705880
null === newChild.key &&
5881+
(enableFragmentRefs ? void 0 === newChild.props.ref : 1) &&
58715882
(validateFragmentProps(newChild, null, returnFiber),
58725883
(newChild = newChild.props.children));
58735884
if ("object" === typeof newChild && null !== newChild) {
@@ -5888,6 +5899,7 @@ __DEV__ &&
58885899
currentFirstChild,
58895900
newChild.props.children
58905901
);
5902+
enableFragmentRefs && coerceRef(lanes, newChild);
58915903
lanes.return = returnFiber;
58925904
lanes._debugOwner = newChild._owner;
58935905
lanes._debugInfo = currentDebugInfo;
@@ -5930,6 +5942,7 @@ __DEV__ &&
59305942
lanes,
59315943
newChild.key
59325944
)),
5945+
enableFragmentRefs && coerceRef(lanes, newChild),
59335946
(lanes.return = returnFiber),
59345947
(lanes._debugOwner = returnFiber),
59355948
(lanes._debugTask = returnFiber._debugTask),
@@ -8869,10 +8882,12 @@ __DEV__ &&
88698882
);
88708883
case 7:
88718884
return (
8885+
(returnFiber = workInProgress.pendingProps),
8886+
enableFragmentRefs && markRef(current, workInProgress),
88728887
reconcileChildren(
88738888
current,
88748889
workInProgress,
8875-
workInProgress.pendingProps,
8890+
returnFiber,
88768891
renderLanes
88778892
),
88788893
workInProgress.child
@@ -10309,6 +10324,15 @@ __DEV__ &&
1030910324
instanceToUse = instanceToUse.ref;
1031010325
break;
1031110326
}
10327+
instanceToUse = finishedWork.stateNode;
10328+
break;
10329+
case 7:
10330+
if (enableFragmentRefs) {
10331+
null === finishedWork.stateNode &&
10332+
(finishedWork.stateNode = null);
10333+
instanceToUse = finishedWork.stateNode;
10334+
break;
10335+
}
1031210336
default:
1031310337
instanceToUse = finishedWork.stateNode;
1031410338
}
@@ -10538,34 +10562,54 @@ __DEV__ &&
1053810562
(node = node.sibling);
1053910563
}
1054010564
function commitPlacement(finishedWork) {
10541-
a: {
10542-
for (var parent = finishedWork.return; null !== parent; ) {
10543-
if (isHostParent(parent)) {
10544-
var parentFiber = parent;
10545-
break a;
10546-
}
10547-
parent = parent.return;
10565+
for (
10566+
var hostParentFiber,
10567+
parentFragmentInstances = null,
10568+
parentFiber = finishedWork.return;
10569+
null !== parentFiber;
10570+
10571+
) {
10572+
if (
10573+
enableFragmentRefs &&
10574+
parentFiber &&
10575+
7 === parentFiber.tag &&
10576+
null !== parentFiber.stateNode
10577+
) {
10578+
var fragmentInstance = parentFiber.stateNode;
10579+
null === parentFragmentInstances
10580+
? (parentFragmentInstances = [fragmentInstance])
10581+
: parentFragmentInstances.push(fragmentInstance);
10582+
}
10583+
if (isHostParent(parentFiber)) {
10584+
hostParentFiber = parentFiber;
10585+
break;
1054810586
}
10587+
parentFiber = parentFiber.return;
10588+
}
10589+
if (null == hostParentFiber)
1054910590
throw Error(
1055010591
"Expected to find a host parent. This error is likely caused by a bug in React. Please file an issue."
1055110592
);
10552-
}
10553-
switch (parentFiber.tag) {
10593+
switch (hostParentFiber.tag) {
1055410594
case 27:
1055510595
case 5:
10556-
parent = parentFiber.stateNode;
10557-
parentFiber.flags & 32 && (parentFiber.flags &= -33);
10558-
parentFiber = getHostSibling(finishedWork);
10559-
insertOrAppendPlacementNode(finishedWork, parentFiber, parent);
10596+
parentFragmentInstances = hostParentFiber.stateNode;
10597+
hostParentFiber.flags & 32 && (hostParentFiber.flags &= -33);
10598+
hostParentFiber = getHostSibling(finishedWork);
10599+
insertOrAppendPlacementNode(
10600+
finishedWork,
10601+
hostParentFiber,
10602+
parentFragmentInstances
10603+
);
1056010604
break;
1056110605
case 3:
1056210606
case 4:
10563-
parent = parentFiber.stateNode.containerInfo;
10564-
parentFiber = getHostSibling(finishedWork);
10607+
hostParentFiber = hostParentFiber.stateNode.containerInfo;
10608+
parentFragmentInstances = getHostSibling(finishedWork);
1056510609
insertOrAppendPlacementNodeIntoContainer(
1056610610
finishedWork,
10567-
parentFiber,
10568-
parent
10611+
parentFragmentInstances,
10612+
hostParentFiber
1056910613
);
1057010614
break;
1057110615
default:
@@ -11084,11 +11128,14 @@ __DEV__ &&
1108411128
: safelyDetachRef(finishedWork, finishedWork.return));
1108511129
break;
1108611130
case 30:
11087-
if (enableViewTransition) {
11088-
recursivelyTraverseLayoutEffects(finishedRoot, finishedWork);
11089-
flags & 512 && safelyAttachRef(finishedWork, finishedWork.return);
11090-
break;
11091-
}
11131+
enableViewTransition &&
11132+
(recursivelyTraverseLayoutEffects(finishedRoot, finishedWork),
11133+
flags & 512 && safelyAttachRef(finishedWork, finishedWork.return));
11134+
break;
11135+
case 7:
11136+
enableFragmentRefs &&
11137+
flags & 512 &&
11138+
safelyAttachRef(finishedWork, finishedWork.return);
1109211139
default:
1109311140
recursivelyTraverseLayoutEffects(finishedRoot, finishedWork);
1109411141
}
@@ -11485,6 +11532,10 @@ __DEV__ &&
1148511532
);
1148611533
offscreenSubtreeWasHidden = _prevHostParent;
1148711534
break;
11535+
case 7:
11536+
enableFragmentRefs &&
11537+
(offscreenSubtreeWasHidden ||
11538+
safelyDetachRef(deletedFiber, nearestMountedAncestor));
1148811539
default:
1148911540
recursivelyTraverseDeletionEffects(
1149011541
finishedRoot,
@@ -11885,25 +11936,24 @@ __DEV__ &&
1188511936
attachSuspenseRetryListeners(finishedWork, flags)));
1188611937
break;
1188711938
case 30:
11888-
if (enableViewTransition) {
11889-
flags & 512 &&
11939+
enableViewTransition &&
11940+
(flags & 512 &&
1189011941
(offscreenSubtreeWasHidden ||
1189111942
null === current ||
11892-
safelyDetachRef(current, current.return));
11943+
safelyDetachRef(current, current.return)),
1189311944
enableViewTransition
1189411945
? ((flags = viewTransitionMutationContext),
1189511946
(viewTransitionMutationContext = !1))
11896-
: (flags = !1);
11897-
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
11898-
commitReconciliationEffects(finishedWork);
11947+
: (flags = !1),
11948+
recursivelyTraverseMutationEffects(root, finishedWork, lanes),
11949+
commitReconciliationEffects(finishedWork),
1189911950
enableViewTransition &&
1190011951
(lanes & 335544064) === lanes &&
1190111952
null !== current &&
1190211953
viewTransitionMutationContext &&
11903-
(finishedWork.flags |= 4);
11904-
enableViewTransition && (viewTransitionMutationContext = flags);
11905-
break;
11906-
}
11954+
(finishedWork.flags |= 4),
11955+
enableViewTransition && (viewTransitionMutationContext = flags));
11956+
break;
1190711957
case 21:
1190811958
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
1190911959
commitReconciliationEffects(finishedWork);
@@ -11992,6 +12042,11 @@ __DEV__ &&
1199212042
case 30:
1199312043
enableViewTransition &&
1199412044
safelyDetachRef(finishedWork, finishedWork.return);
12045+
recursivelyTraverseDisappearLayoutEffects(finishedWork);
12046+
break;
12047+
case 7:
12048+
enableFragmentRefs &&
12049+
safelyDetachRef(finishedWork, finishedWork.return);
1199512050
default:
1199612051
recursivelyTraverseDisappearLayoutEffects(finishedWork);
1199712052
}
@@ -12129,15 +12184,17 @@ __DEV__ &&
1212912184
safelyAttachRef(finishedWork, finishedWork.return);
1213012185
break;
1213112186
case 30:
12132-
if (enableViewTransition) {
12133-
recursivelyTraverseReappearLayoutEffects(
12187+
enableViewTransition &&
12188+
(recursivelyTraverseReappearLayoutEffects(
1213412189
finishedRoot,
1213512190
finishedWork,
1213612191
includeWorkInProgressEffects
12137-
);
12192+
),
12193+
safelyAttachRef(finishedWork, finishedWork.return));
12194+
break;
12195+
case 7:
12196+
enableFragmentRefs &&
1213812197
safelyAttachRef(finishedWork, finishedWork.return);
12139-
break;
12140-
}
1214112198
default:
1214212199
recursivelyTraverseReappearLayoutEffects(
1214312200
finishedRoot,
@@ -16190,6 +16247,7 @@ __DEV__ &&
1619016247
enableViewTransition = dynamicFeatureFlags.enableViewTransition,
1619116248
enableComponentPerformanceTrack =
1619216249
dynamicFeatureFlags.enableComponentPerformanceTrack,
16250+
enableFragmentRefs = dynamicFeatureFlags.enableFragmentRefs,
1619316251
enableSchedulingProfiler = dynamicFeatureFlags.enableSchedulingProfiler,
1619416252
REACT_LEGACY_ELEMENT_TYPE = Symbol.for("react.element"),
1619516253
REACT_ELEMENT_TYPE = renameElementSymbol
@@ -18486,10 +18544,10 @@ __DEV__ &&
1848618544
(function () {
1848718545
var internals = {
1848818546
bundleType: 1,
18489-
version: "19.1.0-www-classic-ca8f91f6-20250311",
18547+
version: "19.1.0-www-classic-6aa8254b-20250312",
1849018548
rendererPackageName: "react-art",
1849118549
currentDispatcherRef: ReactSharedInternals,
18492-
reconcilerVersion: "19.1.0-www-classic-ca8f91f6-20250311"
18550+
reconcilerVersion: "19.1.0-www-classic-6aa8254b-20250312"
1849318551
};
1849418552
internals.overrideHookState = overrideHookState;
1849518553
internals.overrideHookStateDeletePath = overrideHookStateDeletePath;
@@ -18523,7 +18581,7 @@ __DEV__ &&
1852318581
exports.Shape = Shape;
1852418582
exports.Surface = Surface;
1852518583
exports.Text = Text;
18526-
exports.version = "19.1.0-www-classic-ca8f91f6-20250311";
18584+
exports.version = "19.1.0-www-classic-6aa8254b-20250312";
1852718585
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
1852818586
"function" ===
1852918587
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

0 commit comments

Comments
 (0)