Skip to content

Commit 3761acb

Browse files
authored
Classes consume ref prop during SSR, too (#28731)
Same as #28719 but for SSR.
1 parent 7a2609e commit 3761acb

File tree

2 files changed

+128
-3
lines changed

2 files changed

+128
-3
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @emails react-core
8+
*/
9+
10+
'use strict';
11+
12+
import {insertNodesAndExecuteScripts} from '../test-utils/FizzTestUtils';
13+
14+
// Polyfills for test environment
15+
global.ReadableStream =
16+
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
17+
global.TextEncoder = require('util').TextEncoder;
18+
19+
let React;
20+
let ReactDOMServer;
21+
let Scheduler;
22+
let assertLog;
23+
let container;
24+
25+
describe('ReactClassComponentPropResolutionFizz', () => {
26+
beforeEach(() => {
27+
jest.resetModules();
28+
React = require('react');
29+
Scheduler = require('scheduler');
30+
ReactDOMServer = require('react-dom/server.browser');
31+
assertLog = require('internal-test-utils').assertLog;
32+
container = document.createElement('div');
33+
document.body.appendChild(container);
34+
});
35+
36+
afterEach(() => {
37+
document.body.removeChild(container);
38+
});
39+
40+
async function readIntoContainer(stream) {
41+
const reader = stream.getReader();
42+
let result = '';
43+
while (true) {
44+
const {done, value} = await reader.read();
45+
if (done) {
46+
break;
47+
}
48+
result += Buffer.from(value).toString('utf8');
49+
}
50+
const temp = document.createElement('div');
51+
temp.innerHTML = result;
52+
insertNodesAndExecuteScripts(temp, container, null);
53+
}
54+
55+
function Text({text}) {
56+
Scheduler.log(text);
57+
return text;
58+
}
59+
60+
test('resolves ref and default props before calling lifecycle methods', async () => {
61+
function getPropKeys(props) {
62+
return Object.keys(props).join(', ');
63+
}
64+
65+
class Component extends React.Component {
66+
constructor(props) {
67+
super(props);
68+
Scheduler.log('constructor: ' + getPropKeys(props));
69+
}
70+
UNSAFE_componentWillMount() {
71+
Scheduler.log('componentWillMount: ' + getPropKeys(this.props));
72+
}
73+
render() {
74+
return <Text text={'render: ' + getPropKeys(this.props)} />;
75+
}
76+
}
77+
78+
Component.defaultProps = {
79+
default: 'yo',
80+
};
81+
82+
// `ref` should never appear as a prop. `default` always should.
83+
const ref = React.createRef();
84+
const stream = await ReactDOMServer.renderToReadableStream(
85+
<Component text="Yay" ref={ref} />,
86+
);
87+
await readIntoContainer(stream);
88+
assertLog([
89+
'constructor: text, default',
90+
'componentWillMount: text, default',
91+
'render: text, default',
92+
]);
93+
});
94+
});

packages/react-server/src/ReactFizzServer.js

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,21 +1390,52 @@ function finishClassComponent(
13901390
task.keyPath = prevKeyPath;
13911391
}
13921392

1393+
export function resolveClassComponentProps(
1394+
Component: any,
1395+
baseProps: Object,
1396+
): Object {
1397+
let newProps = baseProps;
1398+
1399+
// TODO: This is where defaultProps should be resolved, too.
1400+
1401+
if (enableRefAsProp) {
1402+
// Remove ref from the props object, if it exists.
1403+
if ('ref' in newProps) {
1404+
newProps = assign({}, newProps);
1405+
delete newProps.ref;
1406+
}
1407+
}
1408+
1409+
return newProps;
1410+
}
1411+
13931412
function renderClassComponent(
13941413
request: Request,
13951414
task: Task,
13961415
keyPath: KeyNode,
13971416
Component: any,
13981417
props: any,
13991418
): void {
1419+
const resolvedProps = resolveClassComponentProps(Component, props);
14001420
const previousComponentStack = task.componentStack;
14011421
task.componentStack = createClassComponentStack(task, Component);
14021422
const maskedContext = !disableLegacyContext
14031423
? getMaskedContext(Component, task.legacyContext)
14041424
: undefined;
1405-
const instance = constructClassInstance(Component, props, maskedContext);
1406-
mountClassInstance(instance, Component, props, maskedContext);
1407-
finishClassComponent(request, task, keyPath, instance, Component, props);
1425+
const instance = constructClassInstance(
1426+
Component,
1427+
resolvedProps,
1428+
maskedContext,
1429+
);
1430+
mountClassInstance(instance, Component, resolvedProps, maskedContext);
1431+
finishClassComponent(
1432+
request,
1433+
task,
1434+
keyPath,
1435+
instance,
1436+
Component,
1437+
resolvedProps,
1438+
);
14081439
task.componentStack = previousComponentStack;
14091440
}
14101441

0 commit comments

Comments
 (0)