Skip to content

Commit 070c9f2

Browse files
committed
Support writing to this.refs from userspace (#28867)
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.
1 parent 7548c01 commit 070c9f2

File tree

4 files changed

+49
-19
lines changed

4 files changed

+49
-19
lines changed

packages/react-reconciler/src/ReactFiberClassComponent.new.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,6 @@ import {
8282

8383
const fakeInternalInstance = {};
8484

85-
// React.Component uses a shared frozen object by default.
86-
// We'll use it to determine whether we need to initialize legacy refs.
87-
export const emptyRefsObject = new React.Component().refs;
88-
8985
let didWarnAboutStateAssignmentForComponent;
9086
let didWarnAboutUninitializedState;
9187
let didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate;
@@ -871,7 +867,6 @@ function mountClassInstance(
871867
const instance = workInProgress.stateNode;
872868
instance.props = newProps;
873869
instance.state = workInProgress.memoizedState;
874-
instance.refs = emptyRefsObject;
875870

876871
initializeUpdateQueue(workInProgress);
877872

packages/react-reconciler/src/ReactFiberClassComponent.old.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,6 @@ import {
8282

8383
const fakeInternalInstance = {};
8484

85-
// React.Component uses a shared frozen object by default.
86-
// We'll use it to determine whether we need to initialize legacy refs.
87-
export const emptyRefsObject = new React.Component().refs;
88-
8985
let didWarnAboutStateAssignmentForComponent;
9086
let didWarnAboutUninitializedState;
9187
let didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate;
@@ -871,7 +867,6 @@ function mountClassInstance(
871867
const instance = workInProgress.stateNode;
872868
instance.props = newProps;
873869
instance.state = workInProgress.memoizedState;
874-
instance.refs = emptyRefsObject;
875870

876871
initializeUpdateQueue(workInProgress);
877872

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @emails react-core
8+
*/
9+
10+
'use strict';
11+
12+
let React;
13+
let ReactNoop;
14+
let act;
15+
16+
describe('ReactFiberRefs', () => {
17+
beforeEach(() => {
18+
jest.resetModules();
19+
React = require('react');
20+
ReactNoop = require('react-noop-renderer');
21+
act = require('jest-react').act;
22+
});
23+
24+
test('strings refs can be codemodded to callback refs', async () => {
25+
let app;
26+
class App extends React.Component {
27+
render() {
28+
app = this;
29+
return (
30+
<div
31+
prop="Hello!"
32+
ref={el => {
33+
// `refs` used to be a shared frozen object unless/until a string
34+
// ref attached by the reconciler, but it's not anymore so that we
35+
// can codemod string refs to userspace callback refs.
36+
this.refs.div = el;
37+
}}
38+
/>
39+
);
40+
}
41+
}
42+
43+
const root = ReactNoop.createRoot();
44+
await act(async () => root.render(<App />));
45+
expect(app.refs.div.prop).toBe('Hello!');
46+
});
47+
});

packages/react/src/ReactBaseClasses.js

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,13 @@
88
import ReactNoopUpdateQueue from './ReactNoopUpdateQueue';
99
import assign from 'shared/assign';
1010

11-
const emptyObject = {};
12-
if (__DEV__) {
13-
Object.freeze(emptyObject);
14-
}
15-
1611
/**
1712
* Base class helpers for the updating state of a component.
1813
*/
1914
function Component(props, context, updater) {
2015
this.props = props;
2116
this.context = context;
22-
// If a component has string refs, we will assign a different object later.
23-
this.refs = emptyObject;
17+
this.refs = {};
2418
// We initialize the default updater but the real one gets injected by the
2519
// renderer.
2620
this.updater = updater || ReactNoopUpdateQueue;
@@ -132,8 +126,7 @@ ComponentDummy.prototype = Component.prototype;
132126
function PureComponent(props, context, updater) {
133127
this.props = props;
134128
this.context = context;
135-
// If a component has string refs, we will assign a different object later.
136-
this.refs = emptyObject;
129+
this.refs = {};
137130
this.updater = updater || ReactNoopUpdateQueue;
138131
}
139132

0 commit comments

Comments
 (0)