Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ref to Fragment (alternative) #32465

Open
wants to merge 15 commits into
base: main
Choose a base branch
from

Conversation

jackpope
Copy link
Contributor

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.

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.

@react-sizebot
Copy link

react-sizebot commented Feb 24, 2025

Comparing: 2980f27...a29bbec

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.js = 6.68 kB 6.68 kB = 1.83 kB 1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 518.24 kB 515.80 kB = 92.43 kB 92.10 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.69 kB 6.69 kB = 1.83 kB 1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js = 570.57 kB 565.64 kB = 101.56 kB 100.84 kB
facebook-www/ReactDOM-prod.classic.js +0.85% 638.06 kB 643.51 kB +0.98% 112.28 kB 113.38 kB
facebook-www/ReactDOM-prod.modern.js +0.87% 628.38 kB 633.83 kB +0.99% 110.70 kB 111.79 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
facebook-www/ReactDOM-prod.modern.js +0.87% 628.38 kB 633.83 kB +0.99% 110.70 kB 111.79 kB
facebook-www/ReactDOM-prod.classic.js +0.85% 638.06 kB 643.51 kB +0.98% 112.28 kB 113.38 kB
facebook-www/ReactDOM-profiling.modern.js +0.83% 655.77 kB 661.22 kB +1.00% 114.58 kB 115.73 kB
facebook-www/ReactDOM-profiling.classic.js +0.82% 665.50 kB 670.95 kB +0.99% 116.18 kB 117.32 kB
facebook-www/ReactDOMTesting-prod.modern.js +0.80% 643.10 kB 648.23 kB +0.87% 114.41 kB 115.40 kB
facebook-www/ReactDOMTesting-prod.classic.js +0.79% 652.78 kB 657.91 kB +0.86% 115.98 kB 116.98 kB
facebook-www/ReactDOM-dev.modern.js +0.55% 1,108.14 kB 1,114.27 kB +0.63% 184.73 kB 185.89 kB
facebook-www/ReactDOM-dev.classic.js +0.55% 1,117.28 kB 1,123.41 kB +0.62% 186.47 kB 187.62 kB
facebook-www/ReactDOMTesting-dev.modern.js +0.51% 1,125.05 kB 1,130.80 kB +0.56% 188.60 kB 189.65 kB
facebook-www/ReactDOMTesting-dev.classic.js +0.51% 1,134.19 kB 1,139.94 kB +0.54% 190.32 kB 191.36 kB
facebook-www/ReactReconciler-prod.modern.js +0.26% 487.13 kB 488.41 kB +0.39% 77.84 kB 78.15 kB
facebook-www/ReactReconciler-prod.classic.js +0.26% 497.39 kB 498.68 kB +0.34% 79.46 kB 79.73 kB
react-native/implementations/ReactNativeRenderer-dev.fb.js = 676.45 kB 674.83 kB = 110.00 kB 109.80 kB
react-native/implementations/ReactNativeRenderer-dev.js = 649.75 kB 648.13 kB = 105.94 kB 105.73 kB
react-native/implementations/ReactNativeRenderer-profiling.fb.js = 410.92 kB 409.81 kB = 70.73 kB 70.64 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-dev.js = 599.00 kB 597.37 kB = 96.55 kB 96.35 kB
facebook-www/ReactTestRenderer-dev.classic.js = 576.00 kB 574.38 kB = 93.72 kB 93.56 kB
facebook-www/ReactTestRenderer-dev.modern.js = 576.00 kB 574.38 kB = 93.72 kB 93.56 kB
react-native/implementations/ReactNativeRenderer-profiling.js = 392.61 kB 391.50 kB = 67.48 kB 67.37 kB
react-native/implementations/ReactNativeRenderer-prod.fb.js = 385.44 kB 384.33 kB = 66.92 kB 66.80 kB
oss-stable/react-art/cjs/react-art.development.js = 560.70 kB 559.08 kB = 90.26 kB 90.07 kB
oss-stable-semver/react-art/cjs/react-art.development.js = 560.63 kB 559.01 kB = 90.24 kB 90.04 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.development.js = 558.14 kB 556.50 kB = 90.68 kB 90.47 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.development.js = 558.09 kB 556.45 kB = 90.67 kB 90.46 kB
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.development.js = 558.06 kB 556.42 kB = 90.65 kB 90.45 kB
oss-stable/react-reconciler/cjs/react-reconciler.development.js = 646.69 kB 644.75 kB = 103.16 kB 102.94 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.development.js = 646.66 kB 644.72 kB = 103.13 kB 102.92 kB
react-native/implementations/ReactFabric-dev.fb.js = 671.99 kB 669.97 kB = 109.15 kB 108.92 kB
react-native/implementations/ReactNativeRenderer-prod.js = 367.26 kB 366.15 kB = 63.67 kB 63.58 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-profiling.js = 356.71 kB 355.61 kB = 61.39 kB 61.30 kB
react-native/implementations/ReactFabric-dev.js = 641.90 kB 639.88 kB = 104.56 kB 104.36 kB
facebook-react-native/react-dom/cjs/ReactDOMProfiling-dev.js = 1,009.87 kB 1,006.68 kB = 169.68 kB 169.26 kB
facebook-react-native/react-dom/cjs/ReactDOMClient-dev.js = 993.54 kB 990.35 kB = 166.85 kB 166.45 kB
oss-stable/react-dom/cjs/react-dom-profiling.development.js = 969.17 kB 965.98 kB = 163.66 kB 163.23 kB
oss-stable-semver/react-dom/cjs/react-dom-profiling.development.js = 969.04 kB 965.86 kB = 163.63 kB 163.20 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-prod.js = 334.54 kB 333.43 kB = 58.22 kB 58.13 kB
oss-stable/react-dom/cjs/react-dom-client.development.js = 952.73 kB 949.54 kB = 160.83 kB 160.39 kB
oss-stable-semver/react-dom/cjs/react-dom-client.development.js = 952.60 kB 949.41 kB = 160.80 kB 160.37 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer-persistent.development.js = 41.39 kB 41.25 kB = 7.57 kB 7.53 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer-persistent.development.js = 41.38 kB 41.24 kB = 7.56 kB 7.53 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer-persistent.development.js = 41.36 kB 41.22 kB = 7.53 kB 7.50 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer.development.js = 41.25 kB 41.11 kB = 7.55 kB 7.51 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer.development.js = 41.24 kB 41.10 kB = 7.54 kB 7.51 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer.development.js = 41.22 kB 41.08 kB = 7.51 kB 7.48 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer-persistent.production.js = 37.15 kB 37.02 kB = 6.97 kB 6.94 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer-persistent.production.js = 37.14 kB 37.02 kB = 6.97 kB 6.93 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer-persistent.production.js = 37.12 kB 36.99 kB = 6.94 kB 6.91 kB
oss-experimental/react-noop-renderer/cjs/react-noop-renderer.production.js = 37.02 kB 36.89 kB = 6.95 kB 6.92 kB
oss-stable/react-noop-renderer/cjs/react-noop-renderer.production.js = 37.02 kB 36.89 kB = 6.95 kB 6.91 kB
oss-stable-semver/react-noop-renderer/cjs/react-noop-renderer.production.js = 36.99 kB 36.86 kB = 6.92 kB 6.89 kB
oss-stable/react-reconciler/cjs/react-reconciler.profiling.js = 418.86 kB 417.38 kB = 67.30 kB 67.17 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.profiling.js = 418.84 kB 417.36 kB = 67.28 kB 67.14 kB
oss-stable/react-art/cjs/react-art.production.js = 302.31 kB 301.22 kB = 51.44 kB 51.35 kB
oss-stable-semver/react-art/cjs/react-art.production.js = 302.23 kB 301.15 kB = 51.41 kB 51.32 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.production.js = 314.05 kB 312.92 kB = 55.01 kB 54.87 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.production.js = 313.88 kB 312.75 kB = 54.98 kB 54.84 kB
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.production.js = 313.80 kB 312.67 kB = 54.95 kB 54.81 kB
react-native/implementations/ReactFabric-profiling.fb.js = 407.42 kB 405.94 kB = 70.09 kB 69.97 kB
oss-stable/react-reconciler/cjs/react-reconciler.production.js = 392.67 kB 391.19 kB = 63.62 kB 63.51 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.production.js = 392.65 kB 391.17 kB = 63.60 kB 63.49 kB
react-native/implementations/ReactFabric-profiling.js = 385.44 kB 383.95 kB = 66.24 kB 66.12 kB
react-native/implementations/ReactFabric-prod.fb.js = 381.91 kB 380.43 kB = 66.26 kB 66.11 kB
react-native/implementations/ReactFabric-prod.js = 360.20 kB 358.71 kB = 62.52 kB 62.41 kB
facebook-react-native/react-dom/cjs/ReactDOMProfiling-profiling.js = 574.08 kB 571.63 kB = 101.34 kB 100.99 kB
facebook-react-native/react-dom/cjs/ReactDOMClient-profiling.js = 568.14 kB 565.69 kB = 100.18 kB 99.84 kB
facebook-react-native/react-dom/cjs/ReactDOMProfiling-prod.js = 548.78 kB 546.34 kB = 97.50 kB 97.18 kB
oss-stable/react-dom/cjs/react-dom-profiling.profiling.js = 548.65 kB 546.20 kB = 97.14 kB 96.80 kB
oss-stable-semver/react-dom/cjs/react-dom-profiling.profiling.js = 548.52 kB 546.08 kB = 97.11 kB 96.77 kB
facebook-react-native/react-dom/cjs/ReactDOMClient-prod.js = 543.28 kB 540.83 kB = 96.42 kB 96.10 kB
oss-experimental/react-art/cjs/react-art.development.js = 620.46 kB 617.62 kB = 98.70 kB 98.37 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 518.24 kB 515.80 kB = 92.43 kB 92.10 kB
oss-stable-semver/react-dom/cjs/react-dom-client.production.js = 518.12 kB 515.67 kB = 92.41 kB 92.07 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.development.js = 1,062.87 kB 1,057.22 kB = 178.90 kB 178.18 kB
oss-experimental/react-dom/cjs/react-dom-profiling.development.js = 1,062.35 kB 1,056.70 kB = 178.03 kB 177.29 kB
oss-experimental/react-dom/cjs/react-dom-client.development.js = 1,045.95 kB 1,040.31 kB = 175.18 kB 174.44 kB
oss-experimental/react-reconciler/cjs/react-reconciler.development.js = 728.43 kB 723.98 kB = 114.98 kB 114.45 kB
oss-experimental/react-art/cjs/react-art.production.js = 326.42 kB 324.15 kB = 55.65 kB 55.32 kB
oss-experimental/react-dom/cjs/react-dom-profiling.profiling.js = 626.05 kB 621.18 kB = 110.30 kB 109.56 kB
oss-experimental/react-reconciler/cjs/react-reconciler.profiling.js = 491.32 kB 487.24 kB = 78.64 kB 78.16 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.production.js = 585.30 kB 580.37 kB = 105.12 kB 104.42 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js = 570.57 kB 565.64 kB = 101.56 kB 100.84 kB
oss-experimental/react-reconciler/cjs/react-reconciler.production.js = 438.36 kB 434.28 kB = 70.77 kB 70.27 kB

Generated by 🚫 dangerJS against b0de151

Comment on lines +57 to +70
it('accepts a ref callback', async () => {
let fragmentRef;
const root = ReactDOMClient.createRoot(container);

await act(() => {
root.render(
<Fragment ref={ref => (fragmentRef = ref)}>
<div id="child">Hi</div>
</Fragment>,
);
});

expect(fragmentRef.current).not.toEqual(null);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would fragmentRef here have a .current? Seems like it's not an instance of createRef or useRef so I wouldn't expect it to be "boxed" with a .current but instead to be a direct reference to the reference?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants