mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
Add ref to Fragment (#32465)
*This API is experimental and subject to change or removal.* This PR is an alternative to https://github.com/facebook/react/pull/32421 based on feedback: https://github.com/facebook/react/pull/32421#pullrequestreview-2625382015 . 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.
This commit is contained in:
parent
ca8f91f6f6
commit
6aa8254bb7
21
packages/react-art/src/ReactFiberConfigART.js
vendored
21
packages/react-art/src/ReactFiberConfigART.js
vendored
|
|
@ -318,6 +318,27 @@ export function cloneMutableTextInstance(textInstance) {
|
|||
return textInstance;
|
||||
}
|
||||
|
||||
export type FragmentInstanceType = null;
|
||||
|
||||
export function createFragmentInstance(fiber): null {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function updateFragmentInstanceFiber(fiber, instance): void {
|
||||
// Noop
|
||||
}
|
||||
|
||||
export function commitNewChildToFragmentInstance(
|
||||
child,
|
||||
fragmentInstance,
|
||||
): void {
|
||||
// Noop
|
||||
}
|
||||
|
||||
export function deleteChildFromFragmentInstance(child, fragmentInstance): void {
|
||||
// Noop
|
||||
}
|
||||
|
||||
export function finalizeInitialChildren(domElement, type, props) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostCo
|
|||
import hasOwnProperty from 'shared/hasOwnProperty';
|
||||
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
|
||||
import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
|
||||
import {OffscreenComponent} from 'react-reconciler/src/ReactWorkTags';
|
||||
|
||||
export {
|
||||
setCurrentUpdatePriority,
|
||||
|
|
@ -2159,6 +2160,235 @@ export function subscribeToGestureDirection(
|
|||
}
|
||||
}
|
||||
|
||||
type EventListenerOptionsOrUseCapture =
|
||||
| boolean
|
||||
| {
|
||||
capture?: boolean,
|
||||
once?: boolean,
|
||||
passive?: boolean,
|
||||
signal?: AbortSignal,
|
||||
...
|
||||
};
|
||||
|
||||
type StoredEventListener = {
|
||||
type: string,
|
||||
listener: EventListener,
|
||||
optionsOrUseCapture: void | EventListenerOptionsOrUseCapture,
|
||||
};
|
||||
|
||||
export type FragmentInstanceType = {
|
||||
_fragmentFiber: Fiber,
|
||||
_eventListeners: null | Array<StoredEventListener>,
|
||||
addEventListener(
|
||||
type: string,
|
||||
listener: EventListener,
|
||||
optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
|
||||
): void,
|
||||
removeEventListener(
|
||||
type: string,
|
||||
listener: EventListener,
|
||||
optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
|
||||
): void,
|
||||
focus(): void,
|
||||
};
|
||||
|
||||
function FragmentInstance(this: FragmentInstanceType, fragmentFiber: Fiber) {
|
||||
this._fragmentFiber = fragmentFiber;
|
||||
this._eventListeners = null;
|
||||
}
|
||||
// $FlowFixMe[prop-missing]
|
||||
FragmentInstance.prototype.addEventListener = function (
|
||||
this: FragmentInstanceType,
|
||||
type: string,
|
||||
listener: EventListener,
|
||||
optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
|
||||
): void {
|
||||
if (this._eventListeners === null) {
|
||||
this._eventListeners = [];
|
||||
}
|
||||
|
||||
const listeners = this._eventListeners;
|
||||
// Element.addEventListener will only apply uniquely new event listeners by default. Since we
|
||||
// need to collect the listeners to apply to appended children, we track them ourselves and use
|
||||
// custom equality check for the options.
|
||||
const isNewEventListener =
|
||||
indexOfEventListener(listeners, type, listener, optionsOrUseCapture) === -1;
|
||||
if (isNewEventListener) {
|
||||
listeners.push({type, listener, optionsOrUseCapture});
|
||||
traverseFragmentInstanceChildren(
|
||||
this,
|
||||
this._fragmentFiber.child,
|
||||
addEventListenerToChild,
|
||||
type,
|
||||
listener,
|
||||
optionsOrUseCapture,
|
||||
);
|
||||
}
|
||||
this._eventListeners = listeners;
|
||||
};
|
||||
function addEventListenerToChild(
|
||||
child: Instance,
|
||||
type: string,
|
||||
listener: EventListener,
|
||||
optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
|
||||
): boolean {
|
||||
child.addEventListener(type, listener, optionsOrUseCapture);
|
||||
return false;
|
||||
}
|
||||
// $FlowFixMe[prop-missing]
|
||||
FragmentInstance.prototype.removeEventListener = function (
|
||||
this: FragmentInstanceType,
|
||||
type: string,
|
||||
listener: EventListener,
|
||||
optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
|
||||
): void {
|
||||
const listeners = this._eventListeners;
|
||||
if (listeners === null) {
|
||||
return;
|
||||
}
|
||||
if (typeof listeners !== 'undefined' && listeners.length > 0) {
|
||||
traverseFragmentInstanceChildren(
|
||||
this,
|
||||
this._fragmentFiber.child,
|
||||
removeEventListenerFromChild,
|
||||
type,
|
||||
listener,
|
||||
optionsOrUseCapture,
|
||||
);
|
||||
const index = indexOfEventListener(
|
||||
listeners,
|
||||
type,
|
||||
listener,
|
||||
optionsOrUseCapture,
|
||||
);
|
||||
if (this._eventListeners !== null) {
|
||||
this._eventListeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
function removeEventListenerFromChild(
|
||||
child: Instance,
|
||||
type: string,
|
||||
listener: EventListener,
|
||||
optionsOrUseCapture?: EventListenerOptionsOrUseCapture,
|
||||
): boolean {
|
||||
child.removeEventListener(type, listener, optionsOrUseCapture);
|
||||
return false;
|
||||
}
|
||||
// $FlowFixMe[prop-missing]
|
||||
FragmentInstance.prototype.focus = function (this: FragmentInstanceType) {
|
||||
traverseFragmentInstanceChildren(
|
||||
this,
|
||||
this._fragmentFiber.child,
|
||||
setFocusIfFocusable,
|
||||
);
|
||||
};
|
||||
|
||||
function traverseFragmentInstanceChildren<A, B, C>(
|
||||
fragmentInstance: FragmentInstanceType,
|
||||
child: Fiber | null,
|
||||
fn: (Instance, A, B, C) => boolean,
|
||||
a: A,
|
||||
b: B,
|
||||
c: C,
|
||||
): void {
|
||||
while (child !== null) {
|
||||
if (child.tag === HostComponent) {
|
||||
if (fn(child.stateNode, a, b, c)) {
|
||||
return;
|
||||
}
|
||||
} else if (
|
||||
child.tag === OffscreenComponent &&
|
||||
child.memoizedState !== null
|
||||
) {
|
||||
// Skip hidden subtrees
|
||||
} else {
|
||||
traverseFragmentInstanceChildren(
|
||||
fragmentInstance,
|
||||
child.child,
|
||||
fn,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
);
|
||||
}
|
||||
child = child.sibling;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeListenerOptions(
|
||||
opts: ?EventListenerOptionsOrUseCapture,
|
||||
): string {
|
||||
if (opts == null) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
if (typeof opts === 'boolean') {
|
||||
return `c=${opts ? '1' : '0'}`;
|
||||
}
|
||||
|
||||
return `c=${opts.capture ? '1' : '0'}&o=${opts.once ? '1' : '0'}&p=${opts.passive ? '1' : '0'}`;
|
||||
}
|
||||
|
||||
function indexOfEventListener(
|
||||
eventListeners: Array<StoredEventListener>,
|
||||
type: string,
|
||||
listener: EventListener,
|
||||
optionsOrUseCapture: void | EventListenerOptionsOrUseCapture,
|
||||
): number {
|
||||
for (let i = 0; i < eventListeners.length; i++) {
|
||||
const item = eventListeners[i];
|
||||
if (
|
||||
item.type === type &&
|
||||
item.listener === listener &&
|
||||
normalizeListenerOptions(item.optionsOrUseCapture) ===
|
||||
normalizeListenerOptions(optionsOrUseCapture)
|
||||
) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
export function createFragmentInstance(
|
||||
fragmentFiber: Fiber,
|
||||
): FragmentInstanceType {
|
||||
return new (FragmentInstance: any)(fragmentFiber);
|
||||
}
|
||||
|
||||
export function updateFragmentInstanceFiber(
|
||||
fragmentFiber: Fiber,
|
||||
instance: FragmentInstanceType,
|
||||
): void {
|
||||
instance._fragmentFiber = fragmentFiber;
|
||||
}
|
||||
|
||||
export function commitNewChildToFragmentInstance(
|
||||
childElement: Instance,
|
||||
fragmentInstance: FragmentInstanceType,
|
||||
): void {
|
||||
const eventListeners = fragmentInstance._eventListeners;
|
||||
if (eventListeners !== null) {
|
||||
for (let i = 0; i < eventListeners.length; i++) {
|
||||
const {type, listener, optionsOrUseCapture} = eventListeners[i];
|
||||
childElement.addEventListener(type, listener, optionsOrUseCapture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteChildFromFragmentInstance(
|
||||
childElement: Instance,
|
||||
fragmentInstance: FragmentInstanceType,
|
||||
): void {
|
||||
const eventListeners = fragmentInstance._eventListeners;
|
||||
if (eventListeners !== null) {
|
||||
for (let i = 0; i < eventListeners.length; i++) {
|
||||
const {type, listener, optionsOrUseCapture} = eventListeners[i];
|
||||
childElement.removeEventListener(type, listener, optionsOrUseCapture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function clearContainer(container: Container): void {
|
||||
const nodeType = container.nodeType;
|
||||
if (nodeType === DOCUMENT_NODE) {
|
||||
|
|
|
|||
620
packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js
vendored
Normal file
620
packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js
vendored
Normal file
|
|
@ -0,0 +1,620 @@
|
|||
/**
|
||||
* 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.
|
||||
*
|
||||
* @emails reactcore
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
let React;
|
||||
let ReactDOMClient;
|
||||
let act;
|
||||
let container;
|
||||
let Fragment;
|
||||
let Activity;
|
||||
|
||||
describe('FragmentRefs', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
React = require('react');
|
||||
Fragment = React.Fragment;
|
||||
Activity = React.unstable_Activity;
|
||||
ReactDOMClient = require('react-dom/client');
|
||||
act = require('internal-test-utils').act;
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
// @gate enableFragmentRefs
|
||||
it('attaches a ref to Fragment', async () => {
|
||||
const fragmentRef = React.createRef();
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
|
||||
await act(() =>
|
||||
root.render(
|
||||
<div id="parent">
|
||||
<Fragment ref={fragmentRef}>
|
||||
<div id="child">Hi</div>
|
||||
</Fragment>
|
||||
</div>,
|
||||
),
|
||||
);
|
||||
expect(container.innerHTML).toEqual(
|
||||
'<div id="parent"><div id="child">Hi</div></div>',
|
||||
);
|
||||
|
||||
expect(fragmentRef.current).not.toBe(null);
|
||||
});
|
||||
|
||||
// @gate enableFragmentRefs
|
||||
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._fragmentFiber).toBeTruthy();
|
||||
});
|
||||
|
||||
// @gate enableFragmentRefs
|
||||
it('is available in effects', async () => {
|
||||
function Test() {
|
||||
const fragmentRef = React.useRef(null);
|
||||
React.useLayoutEffect(() => {
|
||||
expect(fragmentRef.current).not.toBe(null);
|
||||
});
|
||||
React.useEffect(() => {
|
||||
expect(fragmentRef.current).not.toBe(null);
|
||||
});
|
||||
return (
|
||||
<Fragment ref={fragmentRef}>
|
||||
<div />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(() => root.render(<Test />));
|
||||
});
|
||||
|
||||
describe('focus()', () => {
|
||||
// @gate enableFragmentRefs
|
||||
it('focuses the first focusable child', async () => {
|
||||
const fragmentRef = React.createRef();
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
|
||||
function Test() {
|
||||
return (
|
||||
<div>
|
||||
<Fragment ref={fragmentRef}>
|
||||
<div id="child-a" />
|
||||
<style>{`#child-c {}`}</style>
|
||||
<a id="child-b" href="/">
|
||||
B
|
||||
</a>
|
||||
<a id="child-c" href="/">
|
||||
C
|
||||
</a>
|
||||
</Fragment>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
await act(() => {
|
||||
root.render(<Test />);
|
||||
});
|
||||
|
||||
await act(() => {
|
||||
fragmentRef.current.focus();
|
||||
});
|
||||
expect(document.activeElement.id).toEqual('child-b');
|
||||
document.activeElement.blur();
|
||||
});
|
||||
|
||||
// @gate enableFragmentRefs
|
||||
it('preserves document order when adding and removing children', async () => {
|
||||
const fragmentRef = React.createRef();
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
|
||||
function Test({showA, showB}) {
|
||||
return (
|
||||
<Fragment ref={fragmentRef}>
|
||||
{showA && <a href="/" id="child-a" />}
|
||||
{showB && <a href="/" id="child-b" />}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
// Render with A as the first focusable child
|
||||
await act(() => {
|
||||
root.render(<Test showA={true} showB={false} />);
|
||||
});
|
||||
await act(() => {
|
||||
fragmentRef.current.focus();
|
||||
});
|
||||
expect(document.activeElement.id).toEqual('child-a');
|
||||
document.activeElement.blur();
|
||||
// A is still the first focusable child, but B is also tracked
|
||||
await act(() => {
|
||||
root.render(<Test showA={true} showB={true} />);
|
||||
});
|
||||
await act(() => {
|
||||
fragmentRef.current.focus();
|
||||
});
|
||||
expect(document.activeElement.id).toEqual('child-a');
|
||||
document.activeElement.blur();
|
||||
|
||||
// B is now the first focusable child
|
||||
await act(() => {
|
||||
root.render(<Test showA={false} showB={true} />);
|
||||
});
|
||||
await act(() => {
|
||||
fragmentRef.current.focus();
|
||||
});
|
||||
expect(document.activeElement.id).toEqual('child-b');
|
||||
document.activeElement.blur();
|
||||
});
|
||||
});
|
||||
|
||||
describe('event listeners', () => {
|
||||
// @gate enableFragmentRefs
|
||||
it('adds and removes event listeners from children', async () => {
|
||||
const parentRef = React.createRef();
|
||||
const fragmentRef = React.createRef();
|
||||
const childARef = React.createRef();
|
||||
const childBRef = React.createRef();
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
|
||||
let logs = [];
|
||||
|
||||
function handleFragmentRefClicks() {
|
||||
logs.push('fragmentRef');
|
||||
}
|
||||
|
||||
function Test() {
|
||||
React.useEffect(() => {
|
||||
fragmentRef.current.addEventListener(
|
||||
'click',
|
||||
handleFragmentRefClicks,
|
||||
);
|
||||
|
||||
return () => {
|
||||
fragmentRef.current.removeEventListener(
|
||||
'click',
|
||||
handleFragmentRefClicks,
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<div ref={parentRef}>
|
||||
<Fragment ref={fragmentRef}>
|
||||
<>Text</>
|
||||
<div ref={childARef}>A</div>
|
||||
<>
|
||||
<div ref={childBRef}>B</div>
|
||||
</>
|
||||
</Fragment>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
await act(() => {
|
||||
root.render(<Test />);
|
||||
});
|
||||
|
||||
childARef.current.addEventListener('click', () => {
|
||||
logs.push('A');
|
||||
});
|
||||
|
||||
childBRef.current.addEventListener('click', () => {
|
||||
logs.push('B');
|
||||
});
|
||||
|
||||
// Clicking on the parent should not trigger any listeners
|
||||
parentRef.current.click();
|
||||
expect(logs).toEqual([]);
|
||||
|
||||
// Clicking a child triggers its own listeners and the Fragment's
|
||||
childARef.current.click();
|
||||
expect(logs).toEqual(['fragmentRef', 'A']);
|
||||
|
||||
logs = [];
|
||||
|
||||
childBRef.current.click();
|
||||
expect(logs).toEqual(['fragmentRef', 'B']);
|
||||
|
||||
logs = [];
|
||||
|
||||
fragmentRef.current.removeEventListener('click', handleFragmentRefClicks);
|
||||
|
||||
childARef.current.click();
|
||||
expect(logs).toEqual(['A']);
|
||||
|
||||
logs = [];
|
||||
|
||||
childBRef.current.click();
|
||||
expect(logs).toEqual(['B']);
|
||||
});
|
||||
|
||||
// @gate enableFragmentRefs
|
||||
it('adds and removes event listeners from children with multiple fragments', async () => {
|
||||
const fragmentRef = React.createRef();
|
||||
const nestedFragmentRef = React.createRef();
|
||||
const nestedFragmentRef2 = React.createRef();
|
||||
const childARef = React.createRef();
|
||||
const childBRef = React.createRef();
|
||||
const childCRef = React.createRef();
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
|
||||
await act(() => {
|
||||
root.render(
|
||||
<div>
|
||||
<Fragment ref={fragmentRef}>
|
||||
<div ref={childARef}>A</div>
|
||||
<div>
|
||||
<Fragment ref={nestedFragmentRef}>
|
||||
<div ref={childBRef}>B</div>
|
||||
</Fragment>
|
||||
</div>
|
||||
<Fragment ref={nestedFragmentRef2}>
|
||||
<div ref={childCRef}>C</div>
|
||||
</Fragment>
|
||||
</Fragment>
|
||||
</div>,
|
||||
);
|
||||
});
|
||||
|
||||
let logs = [];
|
||||
|
||||
function handleFragmentRefClicks() {
|
||||
logs.push('fragmentRef');
|
||||
}
|
||||
|
||||
function handleNestedFragmentRefClicks() {
|
||||
logs.push('nestedFragmentRef');
|
||||
}
|
||||
|
||||
function handleNestedFragmentRef2Clicks() {
|
||||
logs.push('nestedFragmentRef2');
|
||||
}
|
||||
|
||||
fragmentRef.current.addEventListener('click', handleFragmentRefClicks);
|
||||
nestedFragmentRef.current.addEventListener(
|
||||
'click',
|
||||
handleNestedFragmentRefClicks,
|
||||
);
|
||||
nestedFragmentRef2.current.addEventListener(
|
||||
'click',
|
||||
handleNestedFragmentRef2Clicks,
|
||||
);
|
||||
|
||||
childBRef.current.click();
|
||||
// Event bubbles to the parent fragment
|
||||
expect(logs).toEqual(['nestedFragmentRef', 'fragmentRef']);
|
||||
|
||||
logs = [];
|
||||
|
||||
childARef.current.click();
|
||||
expect(logs).toEqual(['fragmentRef']);
|
||||
|
||||
logs = [];
|
||||
childCRef.current.click();
|
||||
expect(logs).toEqual(['fragmentRef', 'nestedFragmentRef2']);
|
||||
|
||||
logs = [];
|
||||
|
||||
fragmentRef.current.removeEventListener('click', handleFragmentRefClicks);
|
||||
nestedFragmentRef.current.removeEventListener(
|
||||
'click',
|
||||
handleNestedFragmentRefClicks,
|
||||
);
|
||||
childCRef.current.click();
|
||||
expect(logs).toEqual(['nestedFragmentRef2']);
|
||||
});
|
||||
|
||||
// @gate enableFragmentRefs
|
||||
it('adds an event listener to a newly added child', async () => {
|
||||
const fragmentRef = React.createRef();
|
||||
const childRef = React.createRef();
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
let showChild;
|
||||
|
||||
function Component() {
|
||||
const [shouldShowChild, setShouldShowChild] = React.useState(false);
|
||||
showChild = () => {
|
||||
setShouldShowChild(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Fragment ref={fragmentRef}>
|
||||
<div id="a">A</div>
|
||||
{shouldShowChild && (
|
||||
<div ref={childRef} id="b">
|
||||
B
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
await act(() => {
|
||||
root.render(<Component />);
|
||||
});
|
||||
|
||||
expect(fragmentRef.current).not.toBe(null);
|
||||
expect(childRef.current).toBe(null);
|
||||
|
||||
let hasClicked = false;
|
||||
fragmentRef.current.addEventListener('click', () => {
|
||||
hasClicked = true;
|
||||
});
|
||||
|
||||
await act(() => {
|
||||
showChild();
|
||||
});
|
||||
expect(childRef.current).not.toBe(null);
|
||||
|
||||
childRef.current.click();
|
||||
expect(hasClicked).toBe(true);
|
||||
});
|
||||
|
||||
// @gate enableFragmentRefs
|
||||
it('applies event listeners to host children nested within non-host children', async () => {
|
||||
const fragmentRef = React.createRef();
|
||||
const childRef = React.createRef();
|
||||
const nestedChildRef = React.createRef();
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
|
||||
function Wrapper({children}) {
|
||||
return children;
|
||||
}
|
||||
|
||||
await act(() => {
|
||||
root.render(
|
||||
<div>
|
||||
<Fragment ref={fragmentRef}>
|
||||
<div ref={childRef}>Host A</div>
|
||||
<Wrapper>
|
||||
<Wrapper>
|
||||
<Wrapper>
|
||||
<div ref={nestedChildRef}>Host B</div>
|
||||
</Wrapper>
|
||||
</Wrapper>
|
||||
</Wrapper>
|
||||
</Fragment>
|
||||
</div>,
|
||||
);
|
||||
});
|
||||
const logs = [];
|
||||
fragmentRef.current.addEventListener('click', e => {
|
||||
logs.push(e.target.textContent);
|
||||
});
|
||||
|
||||
expect(logs).toEqual([]);
|
||||
childRef.current.click();
|
||||
expect(logs).toEqual(['Host A']);
|
||||
nestedChildRef.current.click();
|
||||
expect(logs).toEqual(['Host A', 'Host B']);
|
||||
});
|
||||
|
||||
// @gate enableFragmentRefs
|
||||
it('allows adding and cleaning up listeners in effects', async () => {
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
|
||||
let logs = [];
|
||||
function logClick(e) {
|
||||
logs.push(e.currentTarget.id);
|
||||
}
|
||||
|
||||
let rerender;
|
||||
let removeEventListeners;
|
||||
|
||||
function Test() {
|
||||
const fragmentRef = React.useRef(null);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [_, setState] = React.useState(0);
|
||||
rerender = () => {
|
||||
setState(p => p + 1);
|
||||
};
|
||||
removeEventListeners = () => {
|
||||
fragmentRef.current.removeEventListener('click', logClick);
|
||||
};
|
||||
React.useEffect(() => {
|
||||
fragmentRef.current.addEventListener('click', logClick);
|
||||
|
||||
return removeEventListeners;
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment ref={fragmentRef}>
|
||||
<div id="child-a" />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
// The event listener was applied
|
||||
await act(() => root.render(<Test />));
|
||||
expect(logs).toEqual([]);
|
||||
document.querySelector('#child-a').click();
|
||||
expect(logs).toEqual(['child-a']);
|
||||
|
||||
// The event listener can be removed and re-added
|
||||
logs = [];
|
||||
await act(rerender);
|
||||
document.querySelector('#child-a').click();
|
||||
expect(logs).toEqual(['child-a']);
|
||||
});
|
||||
|
||||
// @gate enableFragmentRefs
|
||||
it('does not apply removed event listeners to new children', async () => {
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
const fragmentRef = React.createRef(null);
|
||||
function Test() {
|
||||
return (
|
||||
<Fragment ref={fragmentRef}>
|
||||
<div id="child-a" />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
let logs = [];
|
||||
function logClick(e) {
|
||||
logs.push(e.currentTarget.id);
|
||||
}
|
||||
await act(() => {
|
||||
root.render(<Test />);
|
||||
});
|
||||
fragmentRef.current.addEventListener('click', logClick);
|
||||
const childA = document.querySelector('#child-a');
|
||||
childA.click();
|
||||
expect(logs).toEqual(['child-a']);
|
||||
|
||||
logs = [];
|
||||
fragmentRef.current.removeEventListener('click', logClick);
|
||||
childA.click();
|
||||
expect(logs).toEqual([]);
|
||||
});
|
||||
|
||||
describe('with activity', () => {
|
||||
// @gate enableFragmentRefs && enableActivity
|
||||
it('does not apply event listeners to hidden trees', async () => {
|
||||
const parentRef = React.createRef();
|
||||
const fragmentRef = React.createRef();
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
|
||||
function Test() {
|
||||
return (
|
||||
<div ref={parentRef}>
|
||||
<Fragment ref={fragmentRef}>
|
||||
<div>Child 1</div>
|
||||
<Activity mode="hidden">
|
||||
<div>Child 2</div>
|
||||
</Activity>
|
||||
<div>Child 3</div>
|
||||
</Fragment>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
await act(() => {
|
||||
root.render(<Test />);
|
||||
});
|
||||
|
||||
const logs = [];
|
||||
fragmentRef.current.addEventListener('click', e => {
|
||||
logs.push(e.target.textContent);
|
||||
});
|
||||
|
||||
const [child1, child2, child3] = parentRef.current.children;
|
||||
child1.click();
|
||||
child2.click();
|
||||
child3.click();
|
||||
expect(logs).toEqual(['Child 1', 'Child 3']);
|
||||
});
|
||||
|
||||
// @gate enableFragmentRefs && enableActivity
|
||||
it('applies event listeners to visible trees', async () => {
|
||||
const parentRef = React.createRef();
|
||||
const fragmentRef = React.createRef();
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
|
||||
function Test() {
|
||||
return (
|
||||
<div ref={parentRef}>
|
||||
<Fragment ref={fragmentRef}>
|
||||
<div>Child 1</div>
|
||||
<Activity mode="visible">
|
||||
<div>Child 2</div>
|
||||
</Activity>
|
||||
<div>Child 3</div>
|
||||
</Fragment>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
await act(() => {
|
||||
root.render(<Test />);
|
||||
});
|
||||
|
||||
const logs = [];
|
||||
fragmentRef.current.addEventListener('click', e => {
|
||||
logs.push(e.target.textContent);
|
||||
});
|
||||
|
||||
const [child1, child2, child3] = parentRef.current.children;
|
||||
child1.click();
|
||||
child2.click();
|
||||
child3.click();
|
||||
expect(logs).toEqual(['Child 1', 'Child 2', 'Child 3']);
|
||||
});
|
||||
|
||||
// @gate enableFragmentRefs && enableActivity
|
||||
it('handles Activity modes switching', async () => {
|
||||
const fragmentRef = React.createRef();
|
||||
const fragmentRef2 = React.createRef();
|
||||
const parentRef = React.createRef();
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
|
||||
function Test({mode}) {
|
||||
return (
|
||||
<div id="parent" ref={parentRef}>
|
||||
<Fragment ref={fragmentRef}>
|
||||
<Activity mode={mode}>
|
||||
<div id="child1">Child</div>
|
||||
<Fragment ref={fragmentRef2}>
|
||||
<div id="child2">Child 2</div>
|
||||
</Fragment>
|
||||
</Activity>
|
||||
</Fragment>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
await act(() => {
|
||||
root.render(<Test mode="visible" />);
|
||||
});
|
||||
|
||||
let logs = [];
|
||||
fragmentRef.current.addEventListener('click', () => {
|
||||
logs.push('clicked 1');
|
||||
});
|
||||
fragmentRef2.current.addEventListener('click', () => {
|
||||
logs.push('clicked 2');
|
||||
});
|
||||
parentRef.current.lastChild.click();
|
||||
expect(logs).toEqual(['clicked 1', 'clicked 2']);
|
||||
|
||||
logs = [];
|
||||
await act(() => {
|
||||
root.render(<Test mode="hidden" />);
|
||||
});
|
||||
parentRef.current.firstChild.click();
|
||||
parentRef.current.lastChild.click();
|
||||
expect(logs).toEqual([]);
|
||||
|
||||
logs = [];
|
||||
await act(() => {
|
||||
root.render(<Test mode="visible" />);
|
||||
});
|
||||
parentRef.current.lastChild.click();
|
||||
// Event order is flipped here because the nested child re-registers first
|
||||
expect(logs).toEqual(['clicked 2', 'clicked 1']);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -591,6 +591,35 @@ export function waitForCommitToBeReady(): null {
|
|||
return null;
|
||||
}
|
||||
|
||||
export type FragmentInstanceType = null;
|
||||
|
||||
export function createFragmentInstance(
|
||||
fragmentFiber: Fiber,
|
||||
): FragmentInstanceType {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function updateFragmentInstanceFiber(
|
||||
fragmentFiber: Fiber,
|
||||
instance: FragmentInstanceType,
|
||||
): void {
|
||||
// Noop
|
||||
}
|
||||
|
||||
export function commitNewChildToFragmentInstance(
|
||||
child: PublicInstance,
|
||||
fragmentInstance: FragmentInstanceType,
|
||||
): void {
|
||||
// Noop
|
||||
}
|
||||
|
||||
export function deleteChildFromFragmentInstance(
|
||||
child: PublicInstance,
|
||||
fragmentInstance: FragmentInstanceType,
|
||||
): void {
|
||||
// Noop
|
||||
}
|
||||
|
||||
export const NotPendingTransition: TransitionStatus = null;
|
||||
export const HostTransitionContext: ReactContext<TransitionStatus> = {
|
||||
$$typeof: REACT_CONTEXT_TYPE,
|
||||
|
|
|
|||
|
|
@ -202,6 +202,35 @@ export function cloneMutableTextInstance(
|
|||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export type FragmentInstanceType = null;
|
||||
|
||||
export function createFragmentInstance(
|
||||
fragmentFiber: Fiber,
|
||||
): FragmentInstanceType {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function updateFragmentInstanceFiber(
|
||||
fragmentFiber: Fiber,
|
||||
instance: FragmentInstanceType,
|
||||
): void {
|
||||
// Noop
|
||||
}
|
||||
|
||||
export function commitNewChildToFragmentInstance(
|
||||
child: PublicInstance,
|
||||
fragmentInstance: FragmentInstanceType,
|
||||
): void {
|
||||
// Noop
|
||||
}
|
||||
|
||||
export function deleteChildFromFragmentInstance(
|
||||
child: PublicInstance,
|
||||
fragmentInstance: FragmentInstanceType,
|
||||
): void {
|
||||
// Noop
|
||||
}
|
||||
|
||||
export function finalizeInitialChildren(
|
||||
parentInstance: Instance,
|
||||
type: string,
|
||||
|
|
|
|||
|
|
@ -512,6 +512,18 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
|||
throw new Error('Not yet implemented.');
|
||||
},
|
||||
|
||||
createFragmentInstance(parentInstance) {
|
||||
return null;
|
||||
},
|
||||
|
||||
commitNewChildToFragmentInstance(child, fragmentInstance) {
|
||||
// Noop
|
||||
},
|
||||
|
||||
deleteChildFromFragmentInstance(child, fragmentInstance) {
|
||||
// Noop
|
||||
},
|
||||
|
||||
scheduleTimeout: setTimeout,
|
||||
cancelTimeout: clearTimeout,
|
||||
noTimeout: -1,
|
||||
|
|
|
|||
40
packages/react-reconciler/src/ReactChildFiber.js
vendored
40
packages/react-reconciler/src/ReactChildFiber.js
vendored
|
|
@ -47,6 +47,7 @@ import isArray from 'shared/isArray';
|
|||
import {
|
||||
enableAsyncIterableChildren,
|
||||
disableLegacyMode,
|
||||
enableFragmentRefs,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import {
|
||||
|
|
@ -214,10 +215,14 @@ function validateFragmentProps(
|
|||
const keys = Object.keys(element.props);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
if (key !== 'children' && key !== 'key') {
|
||||
if (
|
||||
key !== 'children' &&
|
||||
key !== 'key' &&
|
||||
(enableFragmentRefs ? key !== 'ref' : true)
|
||||
) {
|
||||
if (fiber === null) {
|
||||
// For unkeyed root fragments there's no Fiber. We create a fake one just for
|
||||
// error stack handling.
|
||||
// For unkeyed root fragments without refs (enableFragmentRefs),
|
||||
// there's no Fiber. We create a fake one just for error stack handling.
|
||||
fiber = createFiberFromElement(element, returnFiber.mode, 0);
|
||||
if (__DEV__) {
|
||||
fiber._debugInfo = currentDebugInfo;
|
||||
|
|
@ -227,11 +232,19 @@ function validateFragmentProps(
|
|||
runWithFiberInDEV(
|
||||
fiber,
|
||||
erroredKey => {
|
||||
if (enableFragmentRefs) {
|
||||
console.error(
|
||||
'Invalid prop `%s` supplied to `React.Fragment`. ' +
|
||||
'React.Fragment can only have `key`, `ref`, and `children` props.',
|
||||
erroredKey,
|
||||
);
|
||||
} else {
|
||||
console.error(
|
||||
'Invalid prop `%s` supplied to `React.Fragment`. ' +
|
||||
'React.Fragment can only have `key` and `children` props.',
|
||||
erroredKey,
|
||||
);
|
||||
}
|
||||
},
|
||||
key,
|
||||
);
|
||||
|
|
@ -517,6 +530,9 @@ function createChildReconciler(
|
|||
lanes,
|
||||
element.key,
|
||||
);
|
||||
if (enableFragmentRefs) {
|
||||
coerceRef(updated, element);
|
||||
}
|
||||
validateFragmentProps(element, updated, returnFiber);
|
||||
return updated;
|
||||
}
|
||||
|
|
@ -1619,6 +1635,9 @@ function createChildReconciler(
|
|||
if (child.tag === Fragment) {
|
||||
deleteRemainingChildren(returnFiber, child.sibling);
|
||||
const existing = useFiber(child, element.props.children);
|
||||
if (enableFragmentRefs) {
|
||||
coerceRef(existing, element);
|
||||
}
|
||||
existing.return = returnFiber;
|
||||
if (__DEV__) {
|
||||
existing._debugOwner = element._owner;
|
||||
|
|
@ -1670,6 +1689,9 @@ function createChildReconciler(
|
|||
lanes,
|
||||
element.key,
|
||||
);
|
||||
if (enableFragmentRefs) {
|
||||
coerceRef(created, element);
|
||||
}
|
||||
created.return = returnFiber;
|
||||
if (__DEV__) {
|
||||
// We treat the parent as the owner for stack purposes.
|
||||
|
|
@ -1742,17 +1764,19 @@ function createChildReconciler(
|
|||
// not as a fragment. Nested arrays on the other hand will be treated as
|
||||
// fragment nodes. Recursion happens at the normal flow.
|
||||
|
||||
// Handle top level unkeyed fragments as if they were arrays.
|
||||
// This leads to an ambiguity between <>{[...]}</> and <>...</>.
|
||||
// Handle top level unkeyed fragments without refs (enableFragmentRefs)
|
||||
// as if they were arrays. This leads to an ambiguity between <>{[...]}</> and <>...</>.
|
||||
// We treat the ambiguous cases above the same.
|
||||
// We don't use recursion here because a fragment inside a fragment
|
||||
// is no longer considered "top level" for these purposes.
|
||||
const isUnkeyedTopLevelFragment =
|
||||
const isUnkeyedUnrefedTopLevelFragment =
|
||||
typeof newChild === 'object' &&
|
||||
newChild !== null &&
|
||||
newChild.type === REACT_FRAGMENT_TYPE &&
|
||||
newChild.key === null;
|
||||
if (isUnkeyedTopLevelFragment) {
|
||||
newChild.key === null &&
|
||||
(enableFragmentRefs ? newChild.props.ref === undefined : true);
|
||||
|
||||
if (isUnkeyedUnrefedTopLevelFragment) {
|
||||
validateFragmentProps(newChild, null, returnFiber);
|
||||
newChild = newChild.props.children;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ import {
|
|||
disableDefaultPropsExceptForClasses,
|
||||
enableHydrationLaneScheduling,
|
||||
enableViewTransition,
|
||||
enableFragmentRefs,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import isArray from 'shared/isArray';
|
||||
import shallowEqual from 'shared/shallowEqual';
|
||||
|
|
@ -987,6 +988,9 @@ function updateFragment(
|
|||
renderLanes: Lanes,
|
||||
) {
|
||||
const nextChildren = workInProgress.pendingProps;
|
||||
if (enableFragmentRefs) {
|
||||
markRef(current, workInProgress);
|
||||
}
|
||||
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
|
||||
return workInProgress.child;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import type {Fiber} from './ReactInternalTypes';
|
|||
import type {UpdateQueue} from './ReactFiberClassUpdateQueue';
|
||||
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks';
|
||||
import type {HookFlags} from './ReactHookEffectTags';
|
||||
import type {FragmentInstanceType} from './ReactFiberConfig';
|
||||
import {
|
||||
getViewTransitionName,
|
||||
type ViewTransitionState,
|
||||
|
|
@ -24,9 +25,11 @@ import {
|
|||
enableSchedulingProfiler,
|
||||
enableUseEffectCRUDOverload,
|
||||
enableViewTransition,
|
||||
enableFragmentRefs,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
ClassComponent,
|
||||
Fragment,
|
||||
HostComponent,
|
||||
HostHoistable,
|
||||
HostSingleton,
|
||||
|
|
@ -48,6 +51,7 @@ import {
|
|||
import {
|
||||
getPublicInstance,
|
||||
createViewTransitionInstance,
|
||||
createFragmentInstance,
|
||||
} from './ReactFiberConfig';
|
||||
import {
|
||||
captureCommitPhaseError,
|
||||
|
|
@ -877,7 +881,7 @@ function commitAttachRef(finishedWork: Fiber) {
|
|||
case HostComponent:
|
||||
instanceToUse = getPublicInstance(finishedWork.stateNode);
|
||||
break;
|
||||
case ViewTransitionComponent:
|
||||
case ViewTransitionComponent: {
|
||||
if (enableViewTransition) {
|
||||
const instance: ViewTransitionState = finishedWork.stateNode;
|
||||
const props: ViewTransitionProps = finishedWork.memoizedProps;
|
||||
|
|
@ -888,6 +892,18 @@ function commitAttachRef(finishedWork: Fiber) {
|
|||
instanceToUse = instance.ref;
|
||||
break;
|
||||
}
|
||||
instanceToUse = finishedWork.stateNode;
|
||||
break;
|
||||
}
|
||||
case Fragment:
|
||||
if (enableFragmentRefs) {
|
||||
const instance: null | FragmentInstanceType = finishedWork.stateNode;
|
||||
if (instance === null) {
|
||||
finishedWork.stateNode = createFragmentInstance(finishedWork);
|
||||
}
|
||||
instanceToUse = finishedWork.stateNode;
|
||||
break;
|
||||
}
|
||||
// Fallthrough
|
||||
default:
|
||||
instanceToUse = finishedWork.stateNode;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import type {
|
|||
SuspenseInstance,
|
||||
Container,
|
||||
ChildSet,
|
||||
FragmentInstanceType,
|
||||
} from './ReactFiberConfig';
|
||||
import type {Fiber, FiberRoot} from './ReactInternalTypes';
|
||||
|
||||
|
|
@ -24,6 +25,7 @@ import {
|
|||
HostText,
|
||||
HostPortal,
|
||||
DehydratedFragment,
|
||||
Fragment,
|
||||
} from './ReactWorkTags';
|
||||
import {ContentReset, Placement} from './ReactFiberFlags';
|
||||
import {
|
||||
|
|
@ -50,11 +52,14 @@ import {
|
|||
acquireSingletonInstance,
|
||||
releaseSingletonInstance,
|
||||
isSingletonScope,
|
||||
commitNewChildToFragmentInstance,
|
||||
deleteChildFromFragmentInstance,
|
||||
} from './ReactFiberConfig';
|
||||
import {captureCommitPhaseError} from './ReactFiberWorkLoop';
|
||||
import {trackHostMutation} from './ReactFiberMutationTracking';
|
||||
|
||||
import {runWithFiberInDEV} from './ReactCurrentFiber';
|
||||
import {enableFragmentRefs} from 'shared/ReactFeatureFlags';
|
||||
|
||||
export function commitHostMount(finishedWork: Fiber) {
|
||||
const type = finishedWork.type;
|
||||
|
|
@ -199,19 +204,46 @@ export function commitShowHideHostTextInstance(node: Fiber, isHidden: boolean) {
|
|||
}
|
||||
}
|
||||
|
||||
function getHostParentFiber(fiber: Fiber): Fiber {
|
||||
export function commitNewChildToFragmentInstances(
|
||||
fiber: Fiber,
|
||||
parentFragmentInstances: Array<FragmentInstanceType>,
|
||||
): void {
|
||||
for (let i = 0; i < parentFragmentInstances.length; i++) {
|
||||
const fragmentInstance = parentFragmentInstances[i];
|
||||
commitNewChildToFragmentInstance(fiber.stateNode, fragmentInstance);
|
||||
}
|
||||
}
|
||||
|
||||
export function commitFragmentInstanceInsertionEffects(fiber: Fiber): void {
|
||||
let parent = fiber.return;
|
||||
while (parent !== null) {
|
||||
if (isHostParent(parent)) {
|
||||
return parent;
|
||||
}
|
||||
parent = parent.return;
|
||||
if (isFragmentInstanceParent(parent)) {
|
||||
const fragmentInstance: FragmentInstanceType = parent.stateNode;
|
||||
commitNewChildToFragmentInstance(fiber.stateNode, fragmentInstance);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Expected to find a host parent. This error is likely caused by a bug ' +
|
||||
'in React. Please file an issue.',
|
||||
);
|
||||
if (isHostParent(parent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
parent = parent.return;
|
||||
}
|
||||
}
|
||||
|
||||
export function commitFragmentInstanceDeletionEffects(fiber: Fiber): void {
|
||||
let parent = fiber.return;
|
||||
while (parent !== null) {
|
||||
if (isFragmentInstanceParent(parent)) {
|
||||
const fragmentInstance: FragmentInstanceType = parent.stateNode;
|
||||
deleteChildFromFragmentInstance(fiber.stateNode, fragmentInstance);
|
||||
}
|
||||
|
||||
if (isHostParent(parent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
parent = parent.return;
|
||||
}
|
||||
}
|
||||
|
||||
function isHostParent(fiber: Fiber): boolean {
|
||||
|
|
@ -226,6 +258,10 @@ function isHostParent(fiber: Fiber): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
function isFragmentInstanceParent(fiber: Fiber): boolean {
|
||||
return fiber && fiber.tag === Fragment && fiber.stateNode !== null;
|
||||
}
|
||||
|
||||
function getHostSibling(fiber: Fiber): ?Instance {
|
||||
// We're going to search forward into the tree until we find a sibling host
|
||||
// node. Unfortunately, if multiple insertions are done in a row we have to
|
||||
|
|
@ -288,6 +324,7 @@ function insertOrAppendPlacementNodeIntoContainer(
|
|||
node: Fiber,
|
||||
before: ?Instance,
|
||||
parent: Container,
|
||||
parentFragmentInstances: null | Array<FragmentInstanceType>,
|
||||
): void {
|
||||
const {tag} = node;
|
||||
const isHost = tag === HostComponent || tag === HostText;
|
||||
|
|
@ -298,6 +335,16 @@ function insertOrAppendPlacementNodeIntoContainer(
|
|||
} else {
|
||||
appendChildToContainer(parent, stateNode);
|
||||
}
|
||||
// TODO: Enable HostText for RN
|
||||
if (
|
||||
enableFragmentRefs &&
|
||||
tag === HostComponent &&
|
||||
// Only run fragment insertion effects for initial insertions
|
||||
node.alternate === null &&
|
||||
parentFragmentInstances !== null
|
||||
) {
|
||||
commitNewChildToFragmentInstances(node, parentFragmentInstances);
|
||||
}
|
||||
trackHostMutation();
|
||||
return;
|
||||
} else if (tag === HostPortal) {
|
||||
|
|
@ -319,10 +366,20 @@ function insertOrAppendPlacementNodeIntoContainer(
|
|||
|
||||
const child = node.child;
|
||||
if (child !== null) {
|
||||
insertOrAppendPlacementNodeIntoContainer(child, before, parent);
|
||||
insertOrAppendPlacementNodeIntoContainer(
|
||||
child,
|
||||
before,
|
||||
parent,
|
||||
parentFragmentInstances,
|
||||
);
|
||||
let sibling = child.sibling;
|
||||
while (sibling !== null) {
|
||||
insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
|
||||
insertOrAppendPlacementNodeIntoContainer(
|
||||
sibling,
|
||||
before,
|
||||
parent,
|
||||
parentFragmentInstances,
|
||||
);
|
||||
sibling = sibling.sibling;
|
||||
}
|
||||
}
|
||||
|
|
@ -332,6 +389,7 @@ function insertOrAppendPlacementNode(
|
|||
node: Fiber,
|
||||
before: ?Instance,
|
||||
parent: Instance,
|
||||
parentFragmentInstances: null | Array<FragmentInstanceType>,
|
||||
): void {
|
||||
const {tag} = node;
|
||||
const isHost = tag === HostComponent || tag === HostText;
|
||||
|
|
@ -342,6 +400,16 @@ function insertOrAppendPlacementNode(
|
|||
} else {
|
||||
appendChild(parent, stateNode);
|
||||
}
|
||||
// TODO: Enable HostText for RN
|
||||
if (
|
||||
enableFragmentRefs &&
|
||||
tag === HostComponent &&
|
||||
// Only run fragment insertion effects for initial insertions
|
||||
node.alternate === null &&
|
||||
parentFragmentInstances !== null
|
||||
) {
|
||||
commitNewChildToFragmentInstances(node, parentFragmentInstances);
|
||||
}
|
||||
trackHostMutation();
|
||||
return;
|
||||
} else if (tag === HostPortal) {
|
||||
|
|
@ -362,10 +430,15 @@ function insertOrAppendPlacementNode(
|
|||
|
||||
const child = node.child;
|
||||
if (child !== null) {
|
||||
insertOrAppendPlacementNode(child, before, parent);
|
||||
insertOrAppendPlacementNode(child, before, parent, parentFragmentInstances);
|
||||
let sibling = child.sibling;
|
||||
while (sibling !== null) {
|
||||
insertOrAppendPlacementNode(sibling, before, parent);
|
||||
insertOrAppendPlacementNode(
|
||||
sibling,
|
||||
before,
|
||||
parent,
|
||||
parentFragmentInstances,
|
||||
);
|
||||
sibling = sibling.sibling;
|
||||
}
|
||||
}
|
||||
|
|
@ -377,40 +450,78 @@ function commitPlacement(finishedWork: Fiber): void {
|
|||
}
|
||||
|
||||
// Recursively insert all host nodes into the parent.
|
||||
const parentFiber = getHostParentFiber(finishedWork);
|
||||
let hostParentFiber;
|
||||
let parentFragmentInstances = null;
|
||||
let parentFiber = finishedWork.return;
|
||||
while (parentFiber !== null) {
|
||||
if (enableFragmentRefs && isFragmentInstanceParent(parentFiber)) {
|
||||
const fragmentInstance: FragmentInstanceType = parentFiber.stateNode;
|
||||
if (parentFragmentInstances === null) {
|
||||
parentFragmentInstances = [fragmentInstance];
|
||||
} else {
|
||||
parentFragmentInstances.push(fragmentInstance);
|
||||
}
|
||||
}
|
||||
if (isHostParent(parentFiber)) {
|
||||
hostParentFiber = parentFiber;
|
||||
break;
|
||||
}
|
||||
parentFiber = parentFiber.return;
|
||||
}
|
||||
if (hostParentFiber == null) {
|
||||
throw new Error(
|
||||
'Expected to find a host parent. This error is likely caused by a bug ' +
|
||||
'in React. Please file an issue.',
|
||||
);
|
||||
}
|
||||
|
||||
switch (parentFiber.tag) {
|
||||
switch (hostParentFiber.tag) {
|
||||
case HostSingleton: {
|
||||
if (supportsSingletons) {
|
||||
const parent: Instance = parentFiber.stateNode;
|
||||
const parent: Instance = hostParentFiber.stateNode;
|
||||
const before = getHostSibling(finishedWork);
|
||||
// We only have the top Fiber that was inserted but we need to recurse down its
|
||||
// children to find all the terminal nodes.
|
||||
insertOrAppendPlacementNode(finishedWork, before, parent);
|
||||
insertOrAppendPlacementNode(
|
||||
finishedWork,
|
||||
before,
|
||||
parent,
|
||||
parentFragmentInstances,
|
||||
);
|
||||
break;
|
||||
}
|
||||
// Fall through
|
||||
}
|
||||
case HostComponent: {
|
||||
const parent: Instance = parentFiber.stateNode;
|
||||
if (parentFiber.flags & ContentReset) {
|
||||
const parent: Instance = hostParentFiber.stateNode;
|
||||
if (hostParentFiber.flags & ContentReset) {
|
||||
// Reset the text content of the parent before doing any insertions
|
||||
resetTextContent(parent);
|
||||
// Clear ContentReset from the effect tag
|
||||
parentFiber.flags &= ~ContentReset;
|
||||
hostParentFiber.flags &= ~ContentReset;
|
||||
}
|
||||
|
||||
const before = getHostSibling(finishedWork);
|
||||
// We only have the top Fiber that was inserted but we need to recurse down its
|
||||
// children to find all the terminal nodes.
|
||||
insertOrAppendPlacementNode(finishedWork, before, parent);
|
||||
insertOrAppendPlacementNode(
|
||||
finishedWork,
|
||||
before,
|
||||
parent,
|
||||
parentFragmentInstances,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case HostRoot:
|
||||
case HostPortal: {
|
||||
const parent: Container = parentFiber.stateNode.containerInfo;
|
||||
const parent: Container = hostParentFiber.stateNode.containerInfo;
|
||||
const before = getHostSibling(finishedWork);
|
||||
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
|
||||
insertOrAppendPlacementNodeIntoContainer(
|
||||
finishedWork,
|
||||
before,
|
||||
parent,
|
||||
parentFragmentInstances,
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ import {
|
|||
disableLegacyMode,
|
||||
enableComponentPerformanceTrack,
|
||||
enableViewTransition,
|
||||
enableFragmentRefs,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
FunctionComponent,
|
||||
|
|
@ -85,6 +86,7 @@ import {
|
|||
CacheComponent,
|
||||
TracingMarkerComponent,
|
||||
ViewTransitionComponent,
|
||||
Fragment,
|
||||
} from './ReactWorkTags';
|
||||
import {
|
||||
NoFlags,
|
||||
|
|
@ -164,6 +166,7 @@ import {
|
|||
cancelRootViewTransitionName,
|
||||
restoreRootViewTransitionName,
|
||||
isSingletonScope,
|
||||
updateFragmentInstanceFiber,
|
||||
} from './ReactFiberConfig';
|
||||
import {
|
||||
captureCommitPhaseError,
|
||||
|
|
@ -235,6 +238,8 @@ import {
|
|||
commitHostRemoveChild,
|
||||
commitHostSingletonAcquisition,
|
||||
commitHostSingletonRelease,
|
||||
commitFragmentInstanceDeletionEffects,
|
||||
commitFragmentInstanceInsertionEffects,
|
||||
} from './ReactFiberCommitHostEffects';
|
||||
import {
|
||||
commitEnterViewTransitions,
|
||||
|
|
@ -767,8 +772,15 @@ function commitLayoutEffectOnFiber(
|
|||
}
|
||||
break;
|
||||
}
|
||||
// Fallthrough
|
||||
break;
|
||||
}
|
||||
case Fragment:
|
||||
if (enableFragmentRefs) {
|
||||
if (flags & Ref) {
|
||||
safelyAttachRef(finishedWork, finishedWork.return);
|
||||
}
|
||||
}
|
||||
// Fallthrough
|
||||
default: {
|
||||
recursivelyTraverseLayoutEffects(
|
||||
finishedRoot,
|
||||
|
|
@ -1353,6 +1365,9 @@ function commitDeletionEffectsOnFiber(
|
|||
if (!offscreenSubtreeWasHidden) {
|
||||
safelyDetachRef(deletedFiber, nearestMountedAncestor);
|
||||
}
|
||||
if (enableFragmentRefs && deletedFiber.tag === HostComponent) {
|
||||
commitFragmentInstanceDeletionEffects(deletedFiber);
|
||||
}
|
||||
// Intentional fallthrough to next branch
|
||||
}
|
||||
case HostText: {
|
||||
|
|
@ -1563,6 +1578,14 @@ function commitDeletionEffectsOnFiber(
|
|||
}
|
||||
break;
|
||||
}
|
||||
case Fragment: {
|
||||
if (enableFragmentRefs) {
|
||||
if (!offscreenSubtreeWasHidden) {
|
||||
safelyDetachRef(deletedFiber, nearestMountedAncestor);
|
||||
}
|
||||
}
|
||||
// Fallthrough
|
||||
}
|
||||
default: {
|
||||
recursivelyTraverseDeletionEffects(
|
||||
finishedRoot,
|
||||
|
|
@ -1947,6 +1970,7 @@ function commitMutationEffectsOnFiber(
|
|||
}
|
||||
case HostComponent: {
|
||||
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
|
||||
|
||||
commitReconciliationEffects(finishedWork, lanes);
|
||||
|
||||
if (flags & Ref) {
|
||||
|
|
@ -2270,7 +2294,7 @@ function commitMutationEffectsOnFiber(
|
|||
}
|
||||
break;
|
||||
}
|
||||
case ViewTransitionComponent:
|
||||
case ViewTransitionComponent: {
|
||||
if (enableViewTransition) {
|
||||
if (flags & Ref) {
|
||||
if (!offscreenSubtreeWasHidden && current !== null) {
|
||||
|
|
@ -2298,7 +2322,8 @@ function commitMutationEffectsOnFiber(
|
|||
popMutationContext(prevMutationContext);
|
||||
break;
|
||||
}
|
||||
// Fallthrough
|
||||
break;
|
||||
}
|
||||
case ScopeComponent: {
|
||||
if (enableScopeAPI) {
|
||||
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
|
||||
|
|
@ -2321,6 +2346,13 @@ function commitMutationEffectsOnFiber(
|
|||
}
|
||||
break;
|
||||
}
|
||||
case Fragment:
|
||||
if (enableFragmentRefs) {
|
||||
if (current && current.stateNode !== null) {
|
||||
updateFragmentInstanceFiber(finishedWork, current.stateNode);
|
||||
}
|
||||
}
|
||||
// Fallthrough
|
||||
default: {
|
||||
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
|
||||
commitReconciliationEffects(finishedWork, lanes);
|
||||
|
|
@ -2638,6 +2670,10 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
|
|||
// TODO (Offscreen) Check: flags & RefStatic
|
||||
safelyDetachRef(finishedWork, finishedWork.return);
|
||||
|
||||
if (enableFragmentRefs && finishedWork.tag === HostComponent) {
|
||||
commitFragmentInstanceDeletionEffects(finishedWork);
|
||||
}
|
||||
|
||||
recursivelyTraverseDisappearLayoutEffects(finishedWork);
|
||||
break;
|
||||
}
|
||||
|
|
@ -2658,6 +2694,13 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
|
|||
if (enableViewTransition) {
|
||||
safelyDetachRef(finishedWork, finishedWork.return);
|
||||
}
|
||||
recursivelyTraverseDisappearLayoutEffects(finishedWork);
|
||||
break;
|
||||
}
|
||||
case Fragment: {
|
||||
if (enableFragmentRefs) {
|
||||
safelyDetachRef(finishedWork, finishedWork.return);
|
||||
}
|
||||
// Fallthrough
|
||||
}
|
||||
default: {
|
||||
|
|
@ -2765,6 +2808,10 @@ export function reappearLayoutEffects(
|
|||
}
|
||||
case HostHoistable:
|
||||
case HostComponent: {
|
||||
// TODO: Enable HostText for RN
|
||||
if (enableFragmentRefs && finishedWork.tag === HostComponent) {
|
||||
commitFragmentInstanceInsertionEffects(finishedWork);
|
||||
}
|
||||
recursivelyTraverseReappearLayoutEffects(
|
||||
finishedRoot,
|
||||
finishedWork,
|
||||
|
|
@ -2857,6 +2904,12 @@ export function reappearLayoutEffects(
|
|||
safelyAttachRef(finishedWork, finishedWork.return);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Fragment: {
|
||||
if (enableFragmentRefs) {
|
||||
safelyAttachRef(finishedWork, finishedWork.return);
|
||||
}
|
||||
// Fallthrough
|
||||
}
|
||||
default: {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export type ViewTransitionInstance = null | {name: string, ...};
|
|||
export opaque type InstanceMeasurement = mixed;
|
||||
export type EventResponder = any;
|
||||
export type GestureTimeline = any;
|
||||
export type FragmentInstanceType = null;
|
||||
|
||||
export const rendererVersion = $$$config.rendererVersion;
|
||||
export const rendererPackageName = $$$config.rendererPackageName;
|
||||
|
|
@ -160,6 +161,13 @@ export const subscribeToGestureDirection =
|
|||
export const createViewTransitionInstance =
|
||||
$$$config.createViewTransitionInstance;
|
||||
export const clearContainer = $$$config.clearContainer;
|
||||
export const createFragmentInstance = $$$config.createFragmentInstance;
|
||||
export const updateFragmentInstanceFiber =
|
||||
$$$config.updateFragmentInstanceFiber;
|
||||
export const commitNewChildToFragmentInstance =
|
||||
$$$config.commitNewChildToFragmentInstance;
|
||||
export const deleteChildFromFragmentInstance =
|
||||
$$$config.deleteChildFromFragmentInstance;
|
||||
|
||||
// -------------------
|
||||
// Persistence
|
||||
|
|
|
|||
|
|
@ -449,6 +449,35 @@ export function createViewTransitionInstance(
|
|||
return null;
|
||||
}
|
||||
|
||||
export type FragmentInstanceType = null;
|
||||
|
||||
export function createFragmentInstance(
|
||||
fragmentFiber: Object,
|
||||
): FragmentInstanceType {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function updateFragmentInstanceFiber(
|
||||
fragmentFiber: Object,
|
||||
instance: FragmentInstanceType,
|
||||
): void {
|
||||
// Noop
|
||||
}
|
||||
|
||||
export function commitNewChildToFragmentInstance(
|
||||
child: Instance,
|
||||
fragmentInstance: FragmentInstanceType,
|
||||
): void {
|
||||
// noop
|
||||
}
|
||||
|
||||
export function deleteChildFromFragmentInstance(
|
||||
child: Instance,
|
||||
fragmentInstance: FragmentInstanceType,
|
||||
): void {
|
||||
// Noop
|
||||
}
|
||||
|
||||
export function getInstanceFromNode(mockNode: Object): Object | null {
|
||||
const instance = nodeToInstanceMap.get(mockNode);
|
||||
if (instance !== undefined) {
|
||||
|
|
|
|||
|
|
@ -427,7 +427,11 @@ describe('ReactElementValidator', () => {
|
|||
const root = ReactDOMClient.createRoot(document.createElement('div'));
|
||||
await act(() => root.render(React.createElement(Foo)));
|
||||
assertConsoleErrorDev([
|
||||
'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' +
|
||||
gate('enableFragmentRefs')
|
||||
? 'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' +
|
||||
'can only have `key`, `ref`, and `children` props.\n' +
|
||||
' in Foo (at **)'
|
||||
: 'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' +
|
||||
'can only have `key` and `children` props.\n' +
|
||||
' in Foo (at **)',
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -221,7 +221,11 @@ describe('ReactJSXElementValidator', () => {
|
|||
root.render(<Foo />);
|
||||
});
|
||||
assertConsoleErrorDev([
|
||||
'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' +
|
||||
gate('enableFragmentRefs')
|
||||
? 'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' +
|
||||
'can only have `key`, `ref`, and `children` props.\n' +
|
||||
' in Foo (at **)'
|
||||
: 'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' +
|
||||
'can only have `key` and `children` props.\n' +
|
||||
' in Foo (at **)',
|
||||
]);
|
||||
|
|
@ -246,11 +250,15 @@ describe('ReactJSXElementValidator', () => {
|
|||
await act(() => {
|
||||
root.render(<Foo />);
|
||||
});
|
||||
assertConsoleErrorDev([
|
||||
assertConsoleErrorDev(
|
||||
gate('enableFragmentRefs')
|
||||
? []
|
||||
: [
|
||||
'Invalid prop `ref` supplied to `React.Fragment`.' +
|
||||
' React.Fragment can only have `key` and `children` props.\n' +
|
||||
' in Foo (at **)',
|
||||
]);
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
it('does not warn for fragments of multiple elements without keys', async () => {
|
||||
|
|
|
|||
|
|
@ -160,9 +160,10 @@ export const enableInfiniteRenderLoopDetection = false;
|
|||
export const enableUseEffectCRUDOverload = false;
|
||||
|
||||
export const enableFastAddPropertiesInDiffing = true;
|
||||
|
||||
export const enableLazyPublicInstanceInFabric = false;
|
||||
|
||||
export const enableFragmentRefs = false;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Ready for next major.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ export const enableThrottledScheduling = false;
|
|||
export const enableViewTransition = false;
|
||||
export const enableSwipeTransition = false;
|
||||
export const enableScrollEndPolyfill = true;
|
||||
export const enableFragmentRefs = false;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
||||
|
|
|
|||
|
|
@ -76,6 +76,8 @@ export const enableFastAddPropertiesInDiffing = false;
|
|||
export const enableLazyPublicInstanceInFabric = false;
|
||||
export const enableScrollEndPolyfill = true;
|
||||
|
||||
export const enableFragmentRefs = false;
|
||||
|
||||
// Profiling Only
|
||||
export const enableProfilerTimer = __PROFILE__;
|
||||
export const enableProfilerCommitHooks = __PROFILE__;
|
||||
|
|
|
|||
|
|
@ -76,6 +76,8 @@ export const enableFastAddPropertiesInDiffing = true;
|
|||
export const enableLazyPublicInstanceInFabric = false;
|
||||
export const enableScrollEndPolyfill = true;
|
||||
|
||||
export const enableFragmentRefs = false;
|
||||
|
||||
// TODO: This must be in sync with the main ReactFeatureFlags file because
|
||||
// the Test Renderer's value must be the same as the one used by the
|
||||
// react package.
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ export const enableSwipeTransition = false;
|
|||
export const enableFastAddPropertiesInDiffing = false;
|
||||
export const enableLazyPublicInstanceInFabric = false;
|
||||
export const enableScrollEndPolyfill = true;
|
||||
export const enableFragmentRefs = false;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
||||
|
|
|
|||
|
|
@ -87,5 +87,7 @@ export const enableFastAddPropertiesInDiffing = false;
|
|||
export const enableLazyPublicInstanceInFabric = false;
|
||||
export const enableScrollEndPolyfill = true;
|
||||
|
||||
export const enableFragmentRefs = false;
|
||||
|
||||
// Flow magic to verify the exports of this file match the original version.
|
||||
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ export const enableLazyPublicInstanceInFabric = false;
|
|||
export const enableViewTransition = __VARIANT__;
|
||||
export const enableComponentPerformanceTrack = __VARIANT__;
|
||||
export const enableScrollEndPolyfill = __VARIANT__;
|
||||
export const enableFragmentRefs = __VARIANT__;
|
||||
|
||||
// TODO: These flags are hard-coded to the default values used in open source.
|
||||
// Update the tests so that they pass in either mode, then set these
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export const {
|
|||
enableViewTransition,
|
||||
enableComponentPerformanceTrack,
|
||||
enableScrollEndPolyfill,
|
||||
enableFragmentRefs,
|
||||
} = dynamicFeatureFlags;
|
||||
|
||||
// On WWW, __EXPERIMENTAL__ is used for a new modern build.
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user