From ea24427d16f3ac9b0f3bb45cdc7919ac208130c9 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 18 Apr 2024 13:37:54 -0400 Subject: [PATCH] Support writing to this.refs from userspace (#28867) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the `refs` property of a class component instance was read-only by user code — only React could write to it, and until/unless a string ref was used, it pointed to a shared empty object that was frozen in dev to prevent userspace mutations. Because string refs are deprecated, we want users to be able to codemod all their string refs to callback refs. The safest way to do this is to output a callback ref that assigns to `this.refs`. So to support this, we need to make `this.refs` writable by userspace. --- .../src/ReactFiberClassComponent.js | 1 - .../src/__tests__/ReactFiberRefs-test.js | 24 +++++++++++++++++++ packages/react/src/ReactBaseClasses.js | 10 ++------ 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 6bf04da4b41b6..ac1b7fb3ab8e7 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -819,7 +819,6 @@ function mountClassInstance( const instance = workInProgress.stateNode; instance.props = newProps; instance.state = workInProgress.memoizedState; - instance.refs = {}; initializeUpdateQueue(workInProgress); diff --git a/packages/react-reconciler/src/__tests__/ReactFiberRefs-test.js b/packages/react-reconciler/src/__tests__/ReactFiberRefs-test.js index e46387d8cc7dd..175c849d94710 100644 --- a/packages/react-reconciler/src/__tests__/ReactFiberRefs-test.js +++ b/packages/react-reconciler/src/__tests__/ReactFiberRefs-test.js @@ -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 ( +
{ + // `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()); + expect(app.refs.div.prop).toBe('Hello!'); + }); }); diff --git a/packages/react/src/ReactBaseClasses.js b/packages/react/src/ReactBaseClasses.js index 7895a97e3a1ef..ce81071937574 100644 --- a/packages/react/src/ReactBaseClasses.js +++ b/packages/react/src/ReactBaseClasses.js @@ -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; }