Skip to content

Commit 181e6a2

Browse files
committed
[Flight]: Client-side registerServerReference must not break .bind()
As a follow-up for facebook#32534 this ensures that calling `registerServerReference` from the client builds with a server reference does not overwrite the `bind` method that was previously defined by the Flight Server. As was already the case with facebook#32534, the compiler must ensure that the client-side `registerServerReference` is called after the server-side `registerServerReference` is called.
1 parent d331ba0 commit 181e6a2

File tree

3 files changed

+111
-56
lines changed

3 files changed

+111
-56
lines changed

packages/react-client/src/ReactFlightReplyClient.js

+81-56
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,13 @@ export type EncodeFormActionCallback = <A>(
6363

6464
export type ServerReferenceId = any;
6565

66-
const knownServerReferences: WeakMap<
67-
Function,
68-
{id: ServerReferenceId, bound: null | Thenable<Array<any>>},
69-
> = new WeakMap();
66+
type ServerReferenceMetaData = {
67+
id: ServerReferenceId,
68+
bound: null | Thenable<Array<any>>,
69+
};
70+
71+
const knownServerReferences: WeakMap<Function, ServerReferenceMetaData> =
72+
new WeakMap();
7073

7174
// Serializable values
7275
export type ReactServerValue =
@@ -864,7 +867,7 @@ export function processReply(
864867
}
865868

866869
const boundCache: WeakMap<
867-
{id: ServerReferenceId, bound: null | Thenable<Array<any>>},
870+
ServerReferenceMetaData,
868871
Thenable<FormData>,
869872
> = new WeakMap();
870873

@@ -905,21 +908,21 @@ function defaultEncodeFormAction(
905908
this: any => Promise<any>,
906909
identifierPrefix: string,
907910
): ReactCustomFormAction {
908-
const reference = knownServerReferences.get(this);
909-
if (!reference) {
911+
const metaData = knownServerReferences.get(this);
912+
if (!metaData) {
910913
throw new Error(
911914
'Tried to encode a Server Action from a different instance than the encoder is from. ' +
912915
'This is a bug in React.',
913916
);
914917
}
915918
let data: null | FormData = null;
916919
let name;
917-
const boundPromise = reference.bound;
920+
const boundPromise = metaData.bound;
918921
if (boundPromise !== null) {
919-
let thenable = boundCache.get(reference);
922+
let thenable = boundCache.get(metaData);
920923
if (!thenable) {
921-
thenable = encodeFormData(reference);
922-
boundCache.set(reference, thenable);
924+
thenable = encodeFormData(metaData);
925+
boundCache.set(metaData, thenable);
923926
}
924927
if (thenable.status === 'rejected') {
925928
throw thenable.reason;
@@ -941,7 +944,7 @@ function defaultEncodeFormAction(
941944
name = '$ACTION_REF_' + identifierPrefix;
942945
} else {
943946
// This is the simple case so we can just encode the ID.
944-
name = '$ACTION_ID_' + reference.id;
947+
name = '$ACTION_ID_' + metaData.id;
945948
}
946949
return {
947950
name: name,
@@ -952,42 +955,42 @@ function defaultEncodeFormAction(
952955
}
953956

954957
function customEncodeFormAction(
955-
proxy: any => Promise<any>,
958+
reference: any => Promise<any>,
956959
identifierPrefix: string,
957960
encodeFormAction: EncodeFormActionCallback,
958961
): ReactCustomFormAction {
959-
const reference = knownServerReferences.get(proxy);
960-
if (!reference) {
962+
const metaData = knownServerReferences.get(reference);
963+
if (!metaData) {
961964
throw new Error(
962965
'Tried to encode a Server Action from a different instance than the encoder is from. ' +
963966
'This is a bug in React.',
964967
);
965968
}
966-
let boundPromise: Promise<Array<any>> = (reference.bound: any);
969+
let boundPromise: Promise<Array<any>> = (metaData.bound: any);
967970
if (boundPromise === null) {
968971
boundPromise = Promise.resolve([]);
969972
}
970-
return encodeFormAction(reference.id, boundPromise);
973+
return encodeFormAction(metaData.id, boundPromise);
971974
}
972975

973976
function isSignatureEqual(
974977
this: any => Promise<any>,
975978
referenceId: ServerReferenceId,
976979
numberOfBoundArgs: number,
977980
): boolean {
978-
const reference = knownServerReferences.get(this);
979-
if (!reference) {
981+
const metaData = knownServerReferences.get(this);
982+
if (!metaData) {
980983
throw new Error(
981984
'Tried to encode a Server Action from a different instance than the encoder is from. ' +
982985
'This is a bug in React.',
983986
);
984987
}
985-
if (reference.id !== referenceId) {
988+
if (metaData.id !== referenceId) {
986989
// These are different functions.
987990
return false;
988991
}
989992
// Now check if the number of bound arguments is the same.
990-
const boundPromise = reference.bound;
993+
const boundPromise = metaData.bound;
991994
if (boundPromise === null) {
992995
// No bound arguments.
993996
return numberOfBoundArgs === 0;
@@ -1134,7 +1137,7 @@ export function registerBoundServerReference<T: Function>(
11341137
// Expose encoder for use by SSR, as well as a special bind that can be used to
11351138
// keep server capabilities.
11361139
if (usedWithSSR) {
1137-
// Only expose this in builds that would actually use it. Not needed on the client.
1140+
// Only expose this in builds that would actually use it. Not needed in the browser.
11381141
const $$FORM_ACTION =
11391142
encodeFormAction === undefined
11401143
? defaultEncodeFormAction
@@ -1151,64 +1154,86 @@ export function registerBoundServerReference<T: Function>(
11511154
Object.defineProperties((reference: any), {
11521155
$$FORM_ACTION: {value: $$FORM_ACTION},
11531156
$$IS_SIGNATURE_EQUAL: {value: isSignatureEqual},
1154-
bind: {value: bind},
11551157
});
1158+
defineBind(reference);
11561159
}
11571160
knownServerReferences.set(reference, {id, bound});
11581161
}
11591162

1163+
// TODO: Ideally we'd use `isServerReference` from
1164+
// 'react-server/src/ReactFlightServerConfig', but that can only be imported
1165+
// in a react-server environment.
1166+
function isServerReference(reference: Object): boolean {
1167+
return reference.$$typeof === Symbol.for('react.server.reference');
1168+
}
1169+
11601170
export function registerServerReference<T: Function>(
11611171
reference: T,
11621172
id: ServerReferenceId,
11631173
encodeFormAction?: EncodeFormActionCallback,
11641174
): ServerReference<T> {
1165-
registerBoundServerReference(reference, id, null, encodeFormAction);
1175+
const bound =
1176+
isServerReference(reference) && reference.$$bound
1177+
? Promise.resolve(reference.$$bound)
1178+
: null;
1179+
1180+
registerBoundServerReference(reference, id, bound, encodeFormAction);
11661181
return reference;
11671182
}
11681183

11691184
// $FlowFixMe[method-unbinding]
11701185
const FunctionBind = Function.prototype.bind;
11711186
// $FlowFixMe[method-unbinding]
11721187
const ArraySlice = Array.prototype.slice;
1173-
function bind(this: Function): Function {
1174-
// $FlowFixMe[unsupported-syntax]
1175-
// $FlowFixMe[prop-missing]
1176-
const newFn = FunctionBind.apply(this, arguments);
1177-
const reference = knownServerReferences.get(this);
1178-
if (reference) {
1179-
if (__DEV__) {
1180-
const thisBind = arguments[0];
1181-
if (thisBind != null) {
1182-
// This doesn't warn in browser environments since it's not instrumented outside
1183-
// usedWithSSR. This makes this an SSR only warning which we don't generally do.
1184-
// TODO: Consider a DEV only instrumentation in the browser.
1185-
console.error(
1186-
'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().',
1188+
1189+
function defineBind<T: Function>(reference: T) {
1190+
// TODO: Instead of checking if it's a server reference, we could also use
1191+
// `reference.hasOwnProperty('bind')`.
1192+
const originalBind = isServerReference(reference)
1193+
? reference.bind
1194+
: FunctionBind;
1195+
1196+
function bind(): Function {
1197+
// $FlowFixMe[prop-missing]
1198+
const newFn = originalBind.apply(reference, arguments);
1199+
const metaData = knownServerReferences.get(reference);
1200+
1201+
if (metaData) {
1202+
if (__DEV__) {
1203+
const thisBind = arguments[0];
1204+
if (thisBind != null) {
1205+
// This doesn't warn in browser environments since it's not
1206+
// instrumented outside usedWithSSR. This makes this an SSR only
1207+
// warning which we don't generally do.
1208+
// TODO: Consider a DEV only instrumentation in the browser.
1209+
console.error(
1210+
'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().',
1211+
);
1212+
}
1213+
}
1214+
const args = ArraySlice.call(arguments, 1);
1215+
let boundPromise = null;
1216+
if (metaData.bound !== null) {
1217+
boundPromise = Promise.resolve((metaData.bound: any)).then(boundArgs =>
1218+
boundArgs.concat(args),
11871219
);
1220+
} else {
1221+
boundPromise = Promise.resolve(args);
11881222
}
1189-
}
1190-
const args = ArraySlice.call(arguments, 1);
1191-
let boundPromise = null;
1192-
if (reference.bound !== null) {
1193-
boundPromise = Promise.resolve((reference.bound: any)).then(boundArgs =>
1194-
boundArgs.concat(args),
1195-
);
1196-
} else {
1197-
boundPromise = Promise.resolve(args);
1198-
}
1199-
// Expose encoder for use by SSR, as well as a special bind that can be used to
1200-
// keep server capabilities.
1201-
if (usedWithSSR) {
1202-
// Only expose this in builds that would actually use it. Not needed on the client.
1223+
// Expose encoder for use by SSR, as well as a special bind that can be
1224+
// used to keep server capabilities.
12031225
Object.defineProperties((newFn: any), {
1204-
$$FORM_ACTION: {value: this.$$FORM_ACTION},
1226+
$$FORM_ACTION: {value: reference.$$FORM_ACTION},
12051227
$$IS_SIGNATURE_EQUAL: {value: isSignatureEqual},
1206-
bind: {value: bind},
12071228
});
1229+
defineBind(newFn);
1230+
knownServerReferences.set(newFn, {id: metaData.id, bound: boundPromise});
12081231
}
1209-
knownServerReferences.set(newFn, {id: reference.id, bound: boundPromise});
1232+
1233+
return newFn;
12101234
}
1211-
return newFn;
1235+
1236+
Object.defineProperty((reference: any), 'bind', {value: bind});
12121237
}
12131238

12141239
export type FindSourceMapURLCallback = (

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js

+7
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,13 @@ describe('ReactFlightDOMEdge', () => {
309309
greet,
310310
});
311311

312+
// Registering the server reference also with the client must not break
313+
// subsequent `.bind` calls.
314+
ReactServerDOMClient.registerServerReference(
315+
ServerModule.greet,
316+
ServerModule.greet.$$id,
317+
);
318+
312319
const stream = await serverAct(() =>
313320
ReactServerDOMServer.renderToReadableStream(
314321
{

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js

+23
Original file line numberDiff line numberDiff line change
@@ -333,4 +333,27 @@ describe('ReactFlightDOMReplyEdge', () => {
333333
expect(replyResult.method).toBe(greet);
334334
expect(replyResult.boundMethod()).toBe('hi, there');
335335
});
336+
337+
it('can pass a bound registered server reference', async () => {
338+
function greet(greeting, name) {
339+
return greeting + ', ' + name;
340+
}
341+
const ServerModule = serverExports({
342+
greet,
343+
});
344+
345+
const boundGreet = ServerModule.greet.bind(null, 'hi');
346+
347+
ReactServerDOMClient.registerServerReference(boundGreet, boundGreet.$$id);
348+
349+
const body = await ReactServerDOMClient.encodeReply({
350+
method: boundGreet,
351+
boundMethod: boundGreet.bind(null, 'there'),
352+
});
353+
const replyResult = await ReactServerDOMServer.decodeReply(
354+
body,
355+
webpackServerMap,
356+
);
357+
expect(replyResult.boundMethod()).toBe('hi, there');
358+
});
336359
});

0 commit comments

Comments
 (0)