Skip to content

Commit

Permalink
Support writing to this.refs from userspace (#28867)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
acdlite committed Apr 23, 2024
1 parent 7548c01 commit d84a900
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 23 deletions.
5 changes: 0 additions & 5 deletions packages/react-reconciler/src/ReactFiberClassComponent.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,6 @@ import {

const fakeInternalInstance = {};

// React.Component uses a shared frozen object by default.
// We'll use it to determine whether we need to initialize legacy refs.
export const emptyRefsObject = new React.Component().refs;

let didWarnAboutStateAssignmentForComponent;
let didWarnAboutUninitializedState;
let didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate;
Expand Down Expand Up @@ -871,7 +867,6 @@ function mountClassInstance(
const instance = workInProgress.stateNode;
instance.props = newProps;
instance.state = workInProgress.memoizedState;
instance.refs = emptyRefsObject;

initializeUpdateQueue(workInProgress);

Expand Down
5 changes: 0 additions & 5 deletions packages/react-reconciler/src/ReactFiberClassComponent.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,6 @@ import {

const fakeInternalInstance = {};

// React.Component uses a shared frozen object by default.
// We'll use it to determine whether we need to initialize legacy refs.
export const emptyRefsObject = new React.Component().refs;

let didWarnAboutStateAssignmentForComponent;
let didWarnAboutUninitializedState;
let didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate;
Expand Down Expand Up @@ -871,7 +867,6 @@ function mountClassInstance(
const instance = workInProgress.stateNode;
instance.props = newProps;
instance.state = workInProgress.memoizedState;
instance.refs = emptyRefsObject;

initializeUpdateQueue(workInProgress);

Expand Down
47 changes: 47 additions & 0 deletions packages/react-reconciler/src/__tests__/ReactFiberRefs-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* 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';

let React;
let ReactNoop;
let act;

describe('ReactFiberRefs', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactNoop = require('react-noop-renderer');
act = require('jest-react').act;
});

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(async () => root.render(<App />));
expect(app.refs.div.prop).toBe('Hello!');
});
});
19 changes: 6 additions & 13 deletions packages/react/src/ReactBaseClasses.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -53,7 +47,7 @@ Component.prototype.isReactComponent = {};
* @final
* @protected
*/
Component.prototype.setState = function(partialState, callback) {
Component.prototype.setState = function (partialState, callback) {
if (
typeof partialState !== 'object' &&
typeof partialState !== 'function' &&
Expand Down Expand Up @@ -82,7 +76,7 @@ Component.prototype.setState = function(partialState, callback) {
* @final
* @protected
*/
Component.prototype.forceUpdate = function(callback) {
Component.prototype.forceUpdate = function (callback) {
this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};

Expand All @@ -104,9 +98,9 @@ if (__DEV__) {
'https://github.com/facebook/react/issues/3236).',
],
};
const defineDeprecationWarning = function(methodName, info) {
const defineDeprecationWarning = function (methodName, info) {
Object.defineProperty(Component.prototype, methodName, {
get: function() {
get: function () {
console.warn(
'%s(...) is deprecated in plain JavaScript React classes. %s',
info[0],
Expand All @@ -132,8 +126,7 @@ ComponentDummy.prototype = Component.prototype;
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;
}

Expand Down

0 comments on commit d84a900

Please sign in to comment.