Skip to content

Commit 2398554

Browse files
authored
[Flight]: Client-side registerServerReference must not break .bind() (#32565)
1 parent 0ca3dee commit 2398554

File tree

2 files changed

+94
-61
lines changed

2 files changed

+94
-61
lines changed

packages/react-client/src/ReactFlightReplyClient.js

Lines changed: 87 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,14 @@ 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 ServerReferenceClosure = {
67+
id: ServerReferenceId,
68+
originalBind: Function,
69+
bound: null | Thenable<Array<any>>,
70+
};
71+
72+
const knownServerReferences: WeakMap<Function, ServerReferenceClosure> =
73+
new WeakMap();
7074

7175
// Serializable values
7276
export type ReactServerValue =
@@ -760,16 +764,17 @@ export function processReply(
760764
}
761765

762766
if (typeof value === 'function') {
763-
const metaData = knownServerReferences.get(value);
764-
if (metaData !== undefined) {
765-
const metaDataJSON = JSON.stringify(metaData, resolveToJSON);
767+
const referenceClosure = knownServerReferences.get(value);
768+
if (referenceClosure !== undefined) {
769+
const {id, bound} = referenceClosure;
770+
const referenceClosureJSON = JSON.stringify({id, bound}, resolveToJSON);
766771
if (formData === null) {
767772
// Upgrade to use FormData to allow us to stream this value.
768773
formData = new FormData();
769774
}
770775
// The reference to this function came from the same client so we can pass it back.
771776
const refId = nextPartId++;
772-
formData.set(formFieldPrefix + refId, metaDataJSON);
777+
formData.set(formFieldPrefix + refId, referenceClosureJSON);
773778
return serializeServerReferenceID(refId);
774779
}
775780
if (temporaryReferences !== undefined && key.indexOf(':') === -1) {
@@ -864,7 +869,7 @@ export function processReply(
864869
}
865870

866871
const boundCache: WeakMap<
867-
{id: ServerReferenceId, bound: null | Thenable<Array<any>>},
872+
ServerReferenceClosure,
868873
Thenable<FormData>,
869874
> = new WeakMap();
870875

@@ -905,21 +910,22 @@ function defaultEncodeFormAction(
905910
this: any => Promise<any>,
906911
identifierPrefix: string,
907912
): ReactCustomFormAction {
908-
const reference = knownServerReferences.get(this);
909-
if (!reference) {
913+
const referenceClosure = knownServerReferences.get(this);
914+
if (!referenceClosure) {
910915
throw new Error(
911916
'Tried to encode a Server Action from a different instance than the encoder is from. ' +
912917
'This is a bug in React.',
913918
);
914919
}
915920
let data: null | FormData = null;
916921
let name;
917-
const boundPromise = reference.bound;
922+
const boundPromise = referenceClosure.bound;
918923
if (boundPromise !== null) {
919-
let thenable = boundCache.get(reference);
924+
let thenable = boundCache.get(referenceClosure);
920925
if (!thenable) {
921-
thenable = encodeFormData(reference);
922-
boundCache.set(reference, thenable);
926+
const {id, bound} = referenceClosure;
927+
thenable = encodeFormData({id, bound});
928+
boundCache.set(referenceClosure, thenable);
923929
}
924930
if (thenable.status === 'rejected') {
925931
throw thenable.reason;
@@ -941,7 +947,7 @@ function defaultEncodeFormAction(
941947
name = '$ACTION_REF_' + identifierPrefix;
942948
} else {
943949
// This is the simple case so we can just encode the ID.
944-
name = '$ACTION_ID_' + reference.id;
950+
name = '$ACTION_ID_' + referenceClosure.id;
945951
}
946952
return {
947953
name: name,
@@ -952,42 +958,42 @@ function defaultEncodeFormAction(
952958
}
953959

954960
function customEncodeFormAction(
955-
proxy: any => Promise<any>,
961+
reference: any => Promise<any>,
956962
identifierPrefix: string,
957963
encodeFormAction: EncodeFormActionCallback,
958964
): ReactCustomFormAction {
959-
const reference = knownServerReferences.get(proxy);
960-
if (!reference) {
965+
const referenceClosure = knownServerReferences.get(reference);
966+
if (!referenceClosure) {
961967
throw new Error(
962968
'Tried to encode a Server Action from a different instance than the encoder is from. ' +
963969
'This is a bug in React.',
964970
);
965971
}
966-
let boundPromise: Promise<Array<any>> = (reference.bound: any);
972+
let boundPromise: Promise<Array<any>> = (referenceClosure.bound: any);
967973
if (boundPromise === null) {
968974
boundPromise = Promise.resolve([]);
969975
}
970-
return encodeFormAction(reference.id, boundPromise);
976+
return encodeFormAction(referenceClosure.id, boundPromise);
971977
}
972978

973979
function isSignatureEqual(
974980
this: any => Promise<any>,
975981
referenceId: ServerReferenceId,
976982
numberOfBoundArgs: number,
977983
): boolean {
978-
const reference = knownServerReferences.get(this);
979-
if (!reference) {
984+
const referenceClosure = knownServerReferences.get(this);
985+
if (!referenceClosure) {
980986
throw new Error(
981987
'Tried to encode a Server Action from a different instance than the encoder is from. ' +
982988
'This is a bug in React.',
983989
);
984990
}
985-
if (reference.id !== referenceId) {
991+
if (referenceClosure.id !== referenceId) {
986992
// These are different functions.
987993
return false;
988994
}
989995
// Now check if the number of bound arguments is the same.
990-
const boundPromise = reference.bound;
996+
const boundPromise = referenceClosure.bound;
991997
if (boundPromise === null) {
992998
// No bound arguments.
993999
return numberOfBoundArgs === 0;
@@ -1131,10 +1137,20 @@ export function registerBoundServerReference<T: Function>(
11311137
bound: null | Thenable<Array<any>>,
11321138
encodeFormAction: void | EncodeFormActionCallback,
11331139
): void {
1140+
if (knownServerReferences.has(reference)) {
1141+
return;
1142+
}
1143+
1144+
knownServerReferences.set(reference, {
1145+
id,
1146+
originalBind: reference.bind,
1147+
bound,
1148+
});
1149+
11341150
// Expose encoder for use by SSR, as well as a special bind that can be used to
11351151
// keep server capabilities.
11361152
if (usedWithSSR) {
1137-
// Only expose this in builds that would actually use it. Not needed on the client.
1153+
// Only expose this in builds that would actually use it. Not needed in the browser.
11381154
const $$FORM_ACTION =
11391155
encodeFormAction === undefined
11401156
? defaultEncodeFormAction
@@ -1154,7 +1170,6 @@ export function registerBoundServerReference<T: Function>(
11541170
bind: {value: bind},
11551171
});
11561172
}
1157-
knownServerReferences.set(reference, {id, bound});
11581173
}
11591174

11601175
export function registerServerReference<T: Function>(
@@ -1171,43 +1186,54 @@ const FunctionBind = Function.prototype.bind;
11711186
// $FlowFixMe[method-unbinding]
11721187
const ArraySlice = Array.prototype.slice;
11731188
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().',
1187-
);
1188-
}
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),
1189+
const referenceClosure = knownServerReferences.get(this);
1190+
1191+
if (!referenceClosure) {
1192+
// $FlowFixMe[prop-missing]
1193+
return FunctionBind.apply(this, arguments);
1194+
}
1195+
1196+
const newFn = referenceClosure.originalBind.apply(this, arguments);
1197+
1198+
if (__DEV__) {
1199+
const thisBind = arguments[0];
1200+
if (thisBind != null) {
1201+
// This doesn't warn in browser environments since it's not instrumented outside
1202+
// usedWithSSR. This makes this an SSR only warning which we don't generally do.
1203+
// TODO: Consider a DEV only instrumentation in the browser.
1204+
console.error(
1205+
'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().',
11951206
);
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.
1203-
Object.defineProperties((newFn: any), {
1204-
$$FORM_ACTION: {value: this.$$FORM_ACTION},
1205-
$$IS_SIGNATURE_EQUAL: {value: isSignatureEqual},
1206-
bind: {value: bind},
1207-
});
12081207
}
1209-
knownServerReferences.set(newFn, {id: reference.id, bound: boundPromise});
12101208
}
1209+
1210+
const args = ArraySlice.call(arguments, 1);
1211+
let boundPromise = null;
1212+
if (referenceClosure.bound !== null) {
1213+
boundPromise = Promise.resolve((referenceClosure.bound: any)).then(
1214+
boundArgs => boundArgs.concat(args),
1215+
);
1216+
} else {
1217+
boundPromise = Promise.resolve(args);
1218+
}
1219+
1220+
knownServerReferences.set(newFn, {
1221+
id: referenceClosure.id,
1222+
originalBind: newFn.bind,
1223+
bound: boundPromise,
1224+
});
1225+
1226+
// Expose encoder for use by SSR, as well as a special bind that can be used to
1227+
// keep server capabilities.
1228+
if (usedWithSSR) {
1229+
// Only expose this in builds that would actually use it. Not needed on the client.
1230+
Object.defineProperties((newFn: any), {
1231+
$$FORM_ACTION: {value: this.$$FORM_ACTION},
1232+
$$IS_SIGNATURE_EQUAL: {value: isSignatureEqual},
1233+
bind: {value: bind},
1234+
});
1235+
}
1236+
12111237
return newFn;
12121238
}
12131239

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

Lines changed: 7 additions & 0 deletions
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
{

0 commit comments

Comments
 (0)