From 3761acb42bf9979314fff130d4d9505408bcb651 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 3 Apr 2024 12:55:57 -0400 Subject: [PATCH] Classes consume ref prop during SSR, too (#28731) Same as #28719 but for SSR. --- ...ctClassComponentPropResolutionFizz-test.js | 94 +++++++++++++++++++ packages/react-server/src/ReactFizzServer.js | 37 +++++++- 2 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 packages/react-dom/src/__tests__/ReactClassComponentPropResolutionFizz-test.js diff --git a/packages/react-dom/src/__tests__/ReactClassComponentPropResolutionFizz-test.js b/packages/react-dom/src/__tests__/ReactClassComponentPropResolutionFizz-test.js new file mode 100644 index 0000000000000..653797ec44b00 --- /dev/null +++ b/packages/react-dom/src/__tests__/ReactClassComponentPropResolutionFizz-test.js @@ -0,0 +1,94 @@ +/** + * 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. + * + * @emails react-core + */ + +'use strict'; + +import {insertNodesAndExecuteScripts} from '../test-utils/FizzTestUtils'; + +// Polyfills for test environment +global.ReadableStream = + require('web-streams-polyfill/ponyfill/es6').ReadableStream; +global.TextEncoder = require('util').TextEncoder; + +let React; +let ReactDOMServer; +let Scheduler; +let assertLog; +let container; + +describe('ReactClassComponentPropResolutionFizz', () => { + beforeEach(() => { + jest.resetModules(); + React = require('react'); + Scheduler = require('scheduler'); + ReactDOMServer = require('react-dom/server.browser'); + assertLog = require('internal-test-utils').assertLog; + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + document.body.removeChild(container); + }); + + async function readIntoContainer(stream) { + const reader = stream.getReader(); + let result = ''; + while (true) { + const {done, value} = await reader.read(); + if (done) { + break; + } + result += Buffer.from(value).toString('utf8'); + } + const temp = document.createElement('div'); + temp.innerHTML = result; + insertNodesAndExecuteScripts(temp, container, null); + } + + function Text({text}) { + Scheduler.log(text); + return text; + } + + test('resolves ref and default props before calling lifecycle methods', async () => { + function getPropKeys(props) { + return Object.keys(props).join(', '); + } + + class Component extends React.Component { + constructor(props) { + super(props); + Scheduler.log('constructor: ' + getPropKeys(props)); + } + UNSAFE_componentWillMount() { + Scheduler.log('componentWillMount: ' + getPropKeys(this.props)); + } + render() { + return ; + } + } + + Component.defaultProps = { + default: 'yo', + }; + + // `ref` should never appear as a prop. `default` always should. + const ref = React.createRef(); + const stream = await ReactDOMServer.renderToReadableStream( + , + ); + await readIntoContainer(stream); + assertLog([ + 'constructor: text, default', + 'componentWillMount: text, default', + 'render: text, default', + ]); + }); +}); diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 7d5659981e400..529eaef8a692b 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -1390,6 +1390,25 @@ function finishClassComponent( task.keyPath = prevKeyPath; } +export function resolveClassComponentProps( + Component: any, + baseProps: Object, +): Object { + let newProps = baseProps; + + // TODO: This is where defaultProps should be resolved, too. + + if (enableRefAsProp) { + // Remove ref from the props object, if it exists. + if ('ref' in newProps) { + newProps = assign({}, newProps); + delete newProps.ref; + } + } + + return newProps; +} + function renderClassComponent( request: Request, task: Task, @@ -1397,14 +1416,26 @@ function renderClassComponent( Component: any, props: any, ): void { + const resolvedProps = resolveClassComponentProps(Component, props); const previousComponentStack = task.componentStack; task.componentStack = createClassComponentStack(task, Component); const maskedContext = !disableLegacyContext ? getMaskedContext(Component, task.legacyContext) : undefined; - const instance = constructClassInstance(Component, props, maskedContext); - mountClassInstance(instance, Component, props, maskedContext); - finishClassComponent(request, task, keyPath, instance, Component, props); + const instance = constructClassInstance( + Component, + resolvedProps, + maskedContext, + ); + mountClassInstance(instance, Component, resolvedProps, maskedContext); + finishClassComponent( + request, + task, + keyPath, + instance, + Component, + resolvedProps, + ); task.componentStack = previousComponentStack; }