Repository: facebook/react
Pull request: Classes consume ref prop during SSR, too

Same as #28719 but for SSR.

Changed files:
filename: packages/react-dom/src/__tests__/ReactClassComponentPropResolutionFizz-test.js
patch: @@ -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 <Text text={'render: ' + getPropKeys(this.props)} />;
+      }
+    }
+
+    Component.defaultProps = {
+      default: 'yo',
+    };
+
+    // `ref` should never appear as a prop. `default` always should.
+    const ref = React.createRef();
+    const stream = await ReactDOMServer.renderToReadableStream(
+      <Component text="Yay" ref={ref} />,
+    );
+    await readIntoContainer(stream);
+    assertLog([
+      'constructor: text, default',
+      'componentWillMount: text, default',
+      'render: text, default',
+    ]);
+  });
+});

filename: packages/react-server/src/ReactFizzServer.js
patch: @@ -1390,21 +1390,52 @@ 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,
   keyPath: KeyNode,
   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;
 }