Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions packages/react-native-fantom/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import {getShadowNode} from '../../react-native/src/private/webapis/dom/nodes/Re
import * as Benchmark from './Benchmark';
import getFantomRenderedOutput from './getFantomRenderedOutput';
import ReactFabric from 'react-native/Libraries/Renderer/shims/ReactFabric';
import NativeFantom from 'react-native/src/private/specs/modules/NativeFantom';
import NativeFantom, {
NativeEventCategory,
} from 'react-native/src/private/specs/modules/NativeFantom';

let globalSurfaceIdCounter = 1;

Expand Down Expand Up @@ -153,9 +155,17 @@ export function dispatchNativeEvent(
node: ReactNativeElement,
type: string,
payload?: {[key: string]: mixed},
options?: {
category?: NativeEventCategory,
},
) {
const shadowNode = getShadowNode(node);
NativeFantom.dispatchNativeEvent(shadowNode, type, payload);
NativeFantom.dispatchNativeEvent(
shadowNode,
type,
payload,
options?.category,
);
}

export const unstable_benchmark = Benchmark;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
/**
* 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
*/

import {NativeEventCategory} from '../../../src/private/specs/modules/NativeFantom';
import ensureInstance from '../../../src/private/utilities/ensureInstance';
import ReactNativeElement from '../../../src/private/webapis/dom/nodes/ReactNativeElement';
import TextInput from '../../Components/TextInput/TextInput';
import Text from '../../Text/Text';
import * as Fantom from '@react-native/fantom';
import * as React from 'react';
import {startTransition, useDeferredValue, useEffect, useState} from 'react';

function ensureReactNativeElement(value: mixed): ReactNativeElement {
return ensureInstance(value, ReactNativeElement);
}

describe('discrete event category', () => {
it('interrupts React rendering and higher priority update is committed first', () => {
const root = Fantom.createRoot();
let maybeTextInputNode;
let importantTextNode;
let deferredTextNode;
let interruptRendering = false;
let effectMock = jest.fn();
let afterUpdate;

function App(props: {text: string}) {
const [text, setText] = useState('initial text');

let deferredText = useDeferredValue(props.text);

if (interruptRendering) {
interruptRendering = false;
const element = ensureReactNativeElement(maybeTextInputNode);
Fantom.runOnUIThread(() => {
Fantom.dispatchNativeEvent(
element,
'change',
{
text: 'update from native',
},
{
category: NativeEventCategory.Discrete,
},
);
});
// We must schedule a task that is run right after the above native event is
// processed to be able to observe the results of rendering.
Fantom.scheduleTask(afterUpdate);
}

useEffect(() => {
effectMock({text, deferredText});
}, [text, deferredText]);

return (
<>
<TextInput
onChangeText={setText}
ref={node => {
maybeTextInputNode = node;
}}
/>
<Text
ref={node => {
importantTextNode = node;
}}>
Important text: {text}
</Text>
<Text
ref={node => {
deferredTextNode = node;
}}>
Deferred text: {deferredText}
</Text>
</>
);
}

Fantom.runTask(() => {
root.render(<App text={'first render'} />);
});

const importantTextNativeElement =
ensureReactNativeElement(importantTextNode);
const deferredTextNativeElement =
ensureReactNativeElement(deferredTextNode);

expect(importantTextNativeElement.textContent).toBe(
'Important text: initial text',
);
expect(deferredTextNativeElement.textContent).toBe(
'Deferred text: first render',
);

interruptRendering = true;

let isImportantTextUpdatedBeforeDeferred = false;

afterUpdate = () => {
isImportantTextUpdatedBeforeDeferred =
importantTextNativeElement.textContent ===
'Important text: update from native' &&
deferredTextNativeElement.textContent === 'Deferred text: first render';
};

Fantom.runTask(() => {
startTransition(() => {
root.render(<App text={'transition'} />);
});
});

expect(isImportantTextUpdatedBeforeDeferred).toBe(true);

expect(effectMock).toHaveBeenCalledTimes(3);
expect(effectMock.mock.calls[0][0]).toEqual({
text: 'initial text',
deferredText: 'first render',
});
expect(effectMock.mock.calls[1][0]).toEqual({
text: 'update from native',
deferredText: 'first render',
});
expect(effectMock.mock.calls[2][0]).toEqual({
text: 'update from native',
deferredText: 'transition',
});
expect(importantTextNativeElement.textContent).toBe(
'Important text: update from native',
);
expect(deferredTextNativeElement.textContent).toBe(
'Deferred text: transition',
);

root.destroy();
});
});

describe('continuous event category', () => {
it('interrupts React rendering but update from continous event is delayed', () => {
const root = Fantom.createRoot();
let maybeTextInputNode;
let importantTextNode;
let deferredTextNode;
let interruptRendering = false;
let effectMock = jest.fn();

function App(props: {text: string}) {
const [text, setText] = useState('initial text');

let deferredText = useDeferredValue(props.text);

if (interruptRendering) {
interruptRendering = false;
const element = ensureReactNativeElement(maybeTextInputNode);
Fantom.runOnUIThread(() => {
Fantom.dispatchNativeEvent(
element,
'selectionChange',
{
selection: {
start: 1,
end: 5,
},
},
{
category: NativeEventCategory.Continuous,
},
);
});
}
useEffect(() => {
effectMock({text, deferredText});
}, [text, deferredText]);

return (
<>
<TextInput
onSelectionChange={event => {
setText(
`start: ${event.nativeEvent.selection.start}, end: ${event.nativeEvent.selection.end}`,
);
}}
ref={node => {
maybeTextInputNode = node;
}}
/>
<Text
ref={node => {
importantTextNode = node;
}}>
Important text: {text}
</Text>
<Text
ref={node => {
deferredTextNode = node;
}}>
Deferred text: {deferredText}
</Text>
</>
);
}

Fantom.runTask(() => {
root.render(<App text={'first render'} />);
});

const importantTextNativeElement =
ensureReactNativeElement(importantTextNode);
const deferredTextNativeElement =
ensureReactNativeElement(deferredTextNode);

expect(importantTextNativeElement.textContent).toBe(
'Important text: initial text',
);
expect(deferredTextNativeElement.textContent).toBe(
'Deferred text: first render',
);

interruptRendering = true;

Fantom.runTask(() => {
startTransition(() => {
root.render(<App text={'transition'} />);
});
});

expect(effectMock).toHaveBeenCalledTimes(3);
expect(effectMock.mock.calls[0][0]).toEqual({
text: 'initial text',
deferredText: 'first render',
});
expect(effectMock.mock.calls[1][0]).toEqual({
text: 'initial text',
deferredText: 'transition',
});
expect(effectMock.mock.calls[2][0]).toEqual({
text: 'start: 1, end: 5',
deferredText: 'transition',
});
expect(importantTextNativeElement.textContent).toBe(
'Important text: start: 1, end: 5',
);
expect(deferredTextNativeElement.textContent).toBe(
'Deferred text: transition',
);

root.destroy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10463,6 +10463,13 @@ exports[`public API should not change unintentionally src/private/specs/modules/
includeRoot: boolean,
includeLayoutMetrics: boolean,
};
export enum NativeEventCategory {
ContinuousStart = 0,
ContinuousEnd = 1,
Unspecified = 2,
Discrete = 3,
Continuous = 4,
}
interface Spec extends TurboModule {
startSurface: (
surfaceId: number,
Expand All @@ -10474,7 +10481,8 @@ interface Spec extends TurboModule {
dispatchNativeEvent: (
shadowNode: mixed,
type: string,
payload?: mixed
payload?: mixed,
category?: NativeEventCategory
) => void;
getMountingManagerLogs: (surfaceId: number) => Array<string>;
flushMessageQueue: () => void;
Expand Down
34 changes: 34 additions & 0 deletions packages/react-native/src/private/specs/modules/NativeFantom.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,39 @@ export type RenderFormatOptions = {
includeLayoutMetrics: boolean,
};

// match RawEvent.h
export enum NativeEventCategory {
/*
* Start of a continuous event. To be used with touchStart.
*/
ContinuousStart = 0,

/*
* End of a continuous event. To be used with touchEnd.
*/
ContinuousEnd = 1,

/*
* Priority for this event will be determined from other events in the
* queue. If it is triggered by continuous event, its priority will be
* default. If it is not triggered by continuous event, its priority will be
* discrete.
*/
Unspecified = 2,

/*
* Forces discrete type for the event. Regardless if continuous event is
* ongoing.
*/
Discrete = 3,

/*
* Forces continuous type for the event. Regardless if continuous event
* isn't ongoing.
*/
Continuous = 4,
}

interface Spec extends TurboModule {
startSurface: (
surfaceId: number,
Expand All @@ -30,6 +63,7 @@ interface Spec extends TurboModule {
shadowNode: mixed /* ShadowNode */,
type: string,
payload?: mixed,
category?: NativeEventCategory,
) => void;
getMountingManagerLogs: (surfaceId: number) => Array<string>;
flushMessageQueue: () => void;
Expand Down