Repository: facebook/react
Pull request: Unrevert "Support writing to this.refs from userspace"

Reverts facebook/react#28877

We found the cause of the regression and should be able to land this again.

Changed files:
filename: packages/react-reconciler/src/ReactFiberClassComponent.js
patch: @@ -19,12 +19,13 @@ import {
 } from './ReactFiberFlags';
 import {
   debugRenderPhaseSideEffectsForStrictMode,
+  disableDefaultPropsExceptForClasses,
   disableLegacyContext,
+  disableStringRefs,
   enableDebugTracing,
-  enableSchedulingProfiler,
   enableLazyContextPropagation,
   enableRefAsProp,
-  disableDefaultPropsExceptForClasses,
+  enableSchedulingProfiler,
 } from 'shared/ReactFeatureFlags';
 import ReactStrictModeWarnings from './ReactStrictModeWarnings';
 import {isMounted} from './ReactFiberTreeReflection';
@@ -819,7 +820,12 @@ function mountClassInstance(
   const instance = workInProgress.stateNode;
   instance.props = newProps;
   instance.state = workInProgress.memoizedState;
-  instance.refs = {};
+  if (!disableStringRefs) {
+    // When string refs are used in create-react-class legacy components,
+    // we need to make refs writable unless we patch all such copies of the
+    // class code that sets to a frozen emptyObject.
+    instance.refs = {};
+  }
 
   initializeUpdateQueue(workInProgress);
 

filename: packages/react-reconciler/src/__tests__/ReactFiberRefs-test.js
patch: @@ -138,4 +138,28 @@ describe('ReactFiberRefs', () => {
     );
     expect(refProp).toBe('child');
   });
+
+  test('strings refs can be codemodded to callback refs', async () => {
+    let app;
+    class App extends React.Component {
+      render() {
+        app = this;
+        return (
+          <div
+            prop="Hello!"
+            ref={el => {
+              // `refs` used to be a shared frozen object unless/until a string
+              // ref attached by the reconciler, but it's not anymore so that we
+              // can codemod string refs to userspace callback refs.
+              this.refs.div = el;
+            }}
+          />
+        );
+      }
+    }
+
+    const root = ReactNoop.createRoot();
+    await act(() => root.render(<App />));
+    expect(app.refs.div.prop).toBe('Hello!');
+  });
 });

filename: packages/react/src/ReactBaseClasses.js
patch: @@ -8,19 +8,13 @@
 import ReactNoopUpdateQueue from './ReactNoopUpdateQueue';
 import assign from 'shared/assign';
 
-const emptyObject = {};
-if (__DEV__) {
-  Object.freeze(emptyObject);
-}
-
 /**
  * Base class helpers for the updating state of a component.
  */
 function Component(props, context, updater) {
   this.props = props;
   this.context = context;
-  // If a component has string refs, we will assign a different object later.
-  this.refs = emptyObject;
+  this.refs = {};
   // We initialize the default updater but the real one gets injected by the
   // renderer.
   this.updater = updater || ReactNoopUpdateQueue;
@@ -133,7 +127,7 @@ function PureComponent(props, context, updater) {
   this.props = props;
   this.context = context;
   // If a component has string refs, we will assign a different object later.
-  this.refs = emptyObject;
+  this.refs = {};
   this.updater = updater || ReactNoopUpdateQueue;
 }