Skip to content

Commit 6c525d1

Browse files
authored
Merge branch 'main' into fix-compiler-path-spaces
2 parents 055ec70 + 428ab82 commit 6c525d1

File tree

9 files changed

+201
-50
lines changed

9 files changed

+201
-50
lines changed

compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
BuiltInSetId,
1818
BuiltInUseActionStateId,
1919
BuiltInUseContextHookId,
20+
BuiltInUseEffectEventId,
2021
BuiltInUseEffectHookId,
2122
BuiltInUseInsertionEffectHookId,
2223
BuiltInUseLayoutEffectHookId,
@@ -27,6 +28,7 @@ import {
2728
BuiltInUseTransitionId,
2829
BuiltInWeakMapId,
2930
BuiltInWeakSetId,
31+
BuiltinEffectEventId,
3032
ReanimatedSharedValueId,
3133
ShapeRegistry,
3234
addFunction,
@@ -722,6 +724,27 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
722724
BuiltInFireId,
723725
),
724726
],
727+
[
728+
'useEffectEvent',
729+
addHook(
730+
DEFAULT_SHAPES,
731+
{
732+
positionalParams: [],
733+
restParam: Effect.Freeze,
734+
returnType: {
735+
kind: 'Function',
736+
return: {kind: 'Poly'},
737+
shapeId: BuiltinEffectEventId,
738+
isConstructor: false,
739+
},
740+
calleeEffect: Effect.Read,
741+
hookKind: 'useEffectEvent',
742+
// Frozen because it should not mutate any locally-bound values
743+
returnValueKind: ValueKind.Frozen,
744+
},
745+
BuiltInUseEffectEventId,
746+
),
747+
],
725748
];
726749

727750
TYPED_GLOBALS.push(

compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1785,6 +1785,13 @@ export function isFireFunctionType(id: Identifier): boolean {
17851785
);
17861786
}
17871787

1788+
export function isEffectEventFunctionType(id: Identifier): boolean {
1789+
return (
1790+
id.type.kind === 'Function' &&
1791+
id.type.shapeId === 'BuiltInEffectEventFunction'
1792+
);
1793+
}
1794+
17881795
export function isStableType(id: Identifier): boolean {
17891796
return (
17901797
isSetStateType(id) ||

compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export type HookKind =
131131
| 'useCallback'
132132
| 'useTransition'
133133
| 'useImperativeHandle'
134+
| 'useEffectEvent'
134135
| 'Custom';
135136

136137
/*
@@ -226,6 +227,8 @@ export const BuiltInUseTransitionId = 'BuiltInUseTransition';
226227
export const BuiltInStartTransitionId = 'BuiltInStartTransition';
227228
export const BuiltInFireId = 'BuiltInFire';
228229
export const BuiltInFireFunctionId = 'BuiltInFireFunction';
230+
export const BuiltInUseEffectEventId = 'BuiltInUseEffectEvent';
231+
export const BuiltinEffectEventId = 'BuiltInEffectEventFunction';
229232

230233
// See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types
231234
export const ReanimatedSharedValueId = 'ReanimatedSharedValueId';
@@ -948,6 +951,19 @@ addObject(BUILTIN_SHAPES, BuiltInRefValueId, [
948951
['*', {kind: 'Object', shapeId: BuiltInRefValueId}],
949952
]);
950953

954+
addFunction(
955+
BUILTIN_SHAPES,
956+
[],
957+
{
958+
positionalParams: [],
959+
restParam: Effect.ConditionallyMutate,
960+
returnType: {kind: 'Poly'},
961+
calleeEffect: Effect.ConditionallyMutate,
962+
returnValueKind: ValueKind.Mutable,
963+
},
964+
BuiltinEffectEventId,
965+
);
966+
951967
/**
952968
* MixedReadOnly =
953969
* | primitive

compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
HIR,
3232
BasicBlock,
3333
BlockId,
34+
isEffectEventFunctionType,
3435
} from '../HIR';
3536
import {collectHoistablePropertyLoadsInInnerFn} from '../HIR/CollectHoistablePropertyLoads';
3637
import {collectOptionalChainSidemap} from '../HIR/CollectOptionalChainDependencies';
@@ -209,7 +210,8 @@ export function inferEffectDependencies(fn: HIRFunction): void {
209210
((isUseRefType(maybeDep.identifier) ||
210211
isSetStateType(maybeDep.identifier)) &&
211212
!reactiveIds.has(maybeDep.identifier.id)) ||
212-
isFireFunctionType(maybeDep.identifier)
213+
isFireFunctionType(maybeDep.identifier) ||
214+
isEffectEventFunctionType(maybeDep.identifier)
213215
) {
214216
// exclude non-reactive hook results, which will never be in a memo block
215217
continue;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @inferEffectDependencies
6+
import {useEffect, useEffectEvent} from 'react';
7+
import {print} from 'shared-runtime';
8+
9+
/**
10+
* We do not include effect events in dep arrays.
11+
*/
12+
function NonReactiveEffectEvent() {
13+
const fn = useEffectEvent(() => print('hello world'));
14+
useEffect(() => fn());
15+
}
16+
17+
```
18+
19+
## Code
20+
21+
```javascript
22+
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
23+
import { useEffect, useEffectEvent } from "react";
24+
import { print } from "shared-runtime";
25+
26+
/**
27+
* We do not include effect events in dep arrays.
28+
*/
29+
function NonReactiveEffectEvent() {
30+
const $ = _c(2);
31+
const fn = useEffectEvent(_temp);
32+
let t0;
33+
if ($[0] !== fn) {
34+
t0 = () => fn();
35+
$[0] = fn;
36+
$[1] = t0;
37+
} else {
38+
t0 = $[1];
39+
}
40+
useEffect(t0, []);
41+
}
42+
function _temp() {
43+
return print("hello world");
44+
}
45+
46+
```
47+
48+
### Eval output
49+
(kind: exception) Fixture not implemented
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// @inferEffectDependencies
2+
import {useEffect, useEffectEvent} from 'react';
3+
import {print} from 'shared-runtime';
4+
5+
/**
6+
* We do not include effect events in dep arrays.
7+
*/
8+
function NonReactiveEffectEvent() {
9+
const fn = useEffectEvent(() => print('hello world'));
10+
useEffect(() => fn());
11+
}

fixtures/flight/src/App.js

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
2-
import {renderToPipeableStream} from 'react-server-dom-webpack/server';
3-
import {createFromNodeStream} from 'react-server-dom-webpack/client';
2+
import {renderToReadableStream} from 'react-server-dom-webpack/server';
3+
import {createFromReadableStream} from 'react-server-dom-webpack/client';
44
import {PassThrough, Readable} from 'stream';
55

66
import Container from './Container.js';
@@ -43,46 +43,55 @@ async function Bar({children}) {
4343
}
4444

4545
async function ThirdPartyComponent() {
46-
return delay('hello from a 3rd party', 30);
46+
return await delay('hello from a 3rd party', 30);
4747
}
4848

49-
// Using Web streams for tee'ing convenience here.
50-
let cachedThirdPartyReadableWeb;
49+
let cachedThirdPartyStream;
5150

5251
// We create the Component outside of AsyncLocalStorage so that it has no owner.
5352
// That way it gets the owner from the call to createFromNodeStream.
5453
const thirdPartyComponent = <ThirdPartyComponent />;
5554

56-
function fetchThirdParty(noCache) {
57-
if (cachedThirdPartyReadableWeb && !noCache) {
58-
const [readableWeb1, readableWeb2] = cachedThirdPartyReadableWeb.tee();
59-
cachedThirdPartyReadableWeb = readableWeb1;
55+
function simulateFetch(cb, latencyMs) {
56+
return new Promise(resolve => {
57+
// Request latency
58+
setTimeout(() => {
59+
const result = cb();
60+
// Response latency
61+
setTimeout(() => {
62+
resolve(result);
63+
}, latencyMs);
64+
}, latencyMs);
65+
});
66+
}
6067

61-
return createFromNodeStream(Readable.fromWeb(readableWeb2), {
62-
moduleMap: {},
63-
moduleLoading: {},
64-
});
68+
async function fetchThirdParty(noCache) {
69+
// We're using the Web Streams APIs for tee'ing convenience.
70+
let stream;
71+
if (cachedThirdPartyStream && !noCache) {
72+
stream = cachedThirdPartyStream;
73+
} else {
74+
stream = await simulateFetch(
75+
() =>
76+
renderToReadableStream(
77+
thirdPartyComponent,
78+
{},
79+
{environmentName: 'third-party'}
80+
),
81+
25
82+
);
6583
}
6684

67-
const stream = renderToPipeableStream(
68-
thirdPartyComponent,
69-
{},
70-
{environmentName: 'third-party'}
71-
);
85+
const [stream1, stream2] = stream.tee();
86+
cachedThirdPartyStream = stream1;
7287

73-
const readable = new PassThrough();
74-
// React currently only supports piping to one stream, so we convert, tee, and
75-
// convert back again.
76-
// TODO: Switch to web streams without converting when #33442 has landed.
77-
const [readableWeb1, readableWeb2] = Readable.toWeb(readable).tee();
78-
cachedThirdPartyReadableWeb = readableWeb1;
79-
const result = createFromNodeStream(Readable.fromWeb(readableWeb2), {
80-
moduleMap: {},
81-
moduleLoading: {},
88+
return createFromReadableStream(stream2, {
89+
serverConsumerManifest: {
90+
moduleMap: {},
91+
serverModuleMap: null,
92+
moduleLoading: null,
93+
},
8294
});
83-
stream.pipe(readable);
84-
85-
return result;
8695
}
8796

8897
async function ServerComponent({noCache}) {

packages/react-native-renderer/src/ReactNativeAttributePayloadFabric.js

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -254,14 +254,19 @@ function diffProperties(
254254
prevProp = prevProps[propKey];
255255
nextProp = nextProps[propKey];
256256

257-
// functions are converted to booleans as markers that the associated
258-
// events should be sent from native.
259257
if (typeof nextProp === 'function') {
260-
nextProp = (true: any);
261-
// If nextProp is not a function, then don't bother changing prevProp
262-
// since nextProp will win and go into the updatePayload regardless.
263-
if (typeof prevProp === 'function') {
264-
prevProp = (true: any);
258+
const attributeConfigHasProcess =
259+
typeof attributeConfig === 'object' &&
260+
typeof attributeConfig.process === 'function';
261+
if (!attributeConfigHasProcess) {
262+
// functions are converted to booleans as markers that the associated
263+
// events should be sent from native.
264+
nextProp = (true: any);
265+
// If nextProp is not a function, then don't bother changing prevProp
266+
// since nextProp will win and go into the updatePayload regardless.
267+
if (typeof prevProp === 'function') {
268+
prevProp = (true: any);
269+
}
265270
}
266271
}
267272

@@ -444,18 +449,22 @@ function addNestedProperty(
444449
} else {
445450
continue;
446451
}
447-
} else if (typeof prop === 'function') {
448-
// A function prop. It represents an event handler. Pass it to native as 'true'.
449-
newValue = true;
450-
} else if (typeof attributeConfig !== 'object') {
451-
// An atomic prop. Doesn't need to be flattened.
452-
newValue = prop;
453-
} else if (typeof attributeConfig.process === 'function') {
454-
// An atomic prop with custom processing.
455-
newValue = attributeConfig.process(prop);
456-
} else if (typeof attributeConfig.diff === 'function') {
457-
// An atomic prop with custom diffing. We don't need to do diffing when adding props.
458-
newValue = prop;
452+
} else if (typeof attributeConfig === 'object') {
453+
if (typeof attributeConfig.process === 'function') {
454+
// An atomic prop with custom processing.
455+
newValue = attributeConfig.process(prop);
456+
} else if (typeof attributeConfig.diff === 'function') {
457+
// An atomic prop with custom diffing. We don't need to do diffing when adding props.
458+
newValue = prop;
459+
}
460+
} else {
461+
if (typeof prop === 'function') {
462+
// A function prop. It represents an event handler. Pass it to native as 'true'.
463+
newValue = true;
464+
} else {
465+
// An atomic prop. Doesn't need to be flattened.
466+
newValue = prop;
467+
}
459468
}
460469

461470
if (newValue !== undefined) {

packages/react-native-renderer/src/__tests__/ReactNativeAttributePayloadFabric-test.internal.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ describe('ReactNativeAttributePayloadFabric.create', () => {
102102
expect(processA).toBeCalledWith(2);
103103
});
104104

105+
it('should use the process attribute for functions as well', () => {
106+
const process = x => x;
107+
const nextFunction = () => {};
108+
expect(create({a: nextFunction}, {a: {process}})).toEqual({
109+
a: nextFunction,
110+
});
111+
});
112+
105113
it('should work with undefined styles', () => {
106114
expect(create({style: undefined}, {style: {b: true}})).toEqual(null);
107115
expect(create({style: {a: '#ffffff', b: 1}}, {style: {b: true}})).toEqual({
@@ -452,4 +460,21 @@ describe('ReactNativeAttributePayloadFabric.diff', () => {
452460
),
453461
).toEqual(null);
454462
});
463+
464+
it('should use the process function config when prop is a function', () => {
465+
const process = jest.fn(a => a);
466+
const nextFunction = function () {};
467+
expect(
468+
diff(
469+
{
470+
a: function () {},
471+
},
472+
{
473+
a: nextFunction,
474+
},
475+
{a: {process}},
476+
),
477+
).toEqual({a: nextFunction});
478+
expect(process).toBeCalled();
479+
});
455480
});

0 commit comments

Comments
 (0)