Skip to content

Commit 9090712

Browse files
committed
Support writing to this.refs from userspace
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 9090712

File tree

5 files changed

+78
-24
lines changed

5 files changed

+78
-24
lines changed

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

+1-6
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import {
3434
createFiberFromText,
3535
createFiberFromPortal,
3636
} from './ReactFiber.new';
37-
import {emptyRefsObject} from './ReactFiberClassComponent.new';
3837
import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading.new';
3938
import {StrictLegacyMode} from './ReactTypeOfMode';
4039
import {getIsHydrating} from './ReactFiberHydrationContext.new';
@@ -199,11 +198,7 @@ function coerceRef(
199198
return current.ref;
200199
}
201200
const ref = function(value) {
202-
let refs = resolvedInst.refs;
203-
if (refs === emptyRefsObject) {
204-
// This is a lazy pooled frozen object, so we need to initialize.
205-
refs = resolvedInst.refs = {};
206-
}
201+
const refs = resolvedInst.refs;
207202
if (value === null) {
208203
delete refs[stringRef];
209204
} else {

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

+1-6
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import {
3434
createFiberFromText,
3535
createFiberFromPortal,
3636
} from './ReactFiber.old';
37-
import {emptyRefsObject} from './ReactFiberClassComponent.old';
3837
import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading.old';
3938
import {StrictLegacyMode} from './ReactTypeOfMode';
4039
import {getIsHydrating} from './ReactFiberHydrationContext.old';
@@ -199,11 +198,7 @@ function coerceRef(
199198
return current.ref;
200199
}
201200
const ref = function(value) {
202-
let refs = resolvedInst.refs;
203-
if (refs === emptyRefsObject) {
204-
// This is a lazy pooled frozen object, so we need to initialize.
205-
refs = resolvedInst.refs = {};
206-
}
201+
const refs = resolvedInst.refs;
207202
if (value === null) {
208203
delete refs[stringRef];
209204
} else {

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

+1-6
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import type {Lanes} from './ReactFiberLane.new';
1212
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.new';
1313
import type {Flags} from './ReactFiberFlags';
1414

15-
import * as React from 'react';
1615
import {
1716
LayoutStatic,
1817
MountLayoutDev,
@@ -82,10 +81,6 @@ import {
8281

8382
const fakeInternalInstance = {};
8483

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-
8984
let didWarnAboutStateAssignmentForComponent;
9085
let didWarnAboutUninitializedState;
9186
let didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate;
@@ -871,7 +866,7 @@ function mountClassInstance(
871866
const instance = workInProgress.stateNode;
872867
instance.props = newProps;
873868
instance.state = workInProgress.memoizedState;
874-
instance.refs = emptyRefsObject;
869+
instance.refs = {};
875870

876871
initializeUpdateQueue(workInProgress);
877872

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

+1-6
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import type {Lanes} from './ReactFiberLane.old';
1212
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old';
1313
import type {Flags} from './ReactFiberFlags';
1414

15-
import * as React from 'react';
1615
import {
1716
LayoutStatic,
1817
MountLayoutDev,
@@ -82,10 +81,6 @@ import {
8281

8382
const fakeInternalInstance = {};
8483

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-
8984
let didWarnAboutStateAssignmentForComponent;
9085
let didWarnAboutUninitializedState;
9186
let didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate;
@@ -871,7 +866,7 @@ function mountClassInstance(
871866
const instance = workInProgress.stateNode;
872867
instance.props = newProps;
873868
instance.state = workInProgress.memoizedState;
874-
instance.refs = emptyRefsObject;
869+
instance.refs = {};
875870

876871
initializeUpdateQueue(workInProgress);
877872

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
48+
test('class refs are initialized to a frozen shared object', async () => {
49+
const refsCollection = new Set();
50+
class Component extends React.Component {
51+
constructor(props) {
52+
super(props);
53+
refsCollection.add(this.refs);
54+
}
55+
render() {
56+
return <div />;
57+
}
58+
}
59+
60+
const root = ReactNoop.createRoot();
61+
await act(() =>
62+
root.render(
63+
<>
64+
<Component />
65+
<Component />
66+
</>,
67+
),
68+
);
69+
70+
expect(refsCollection.size).toBe(1);
71+
const refsInstance = Array.from(refsCollection)[0];
72+
expect(Object.isFrozen(refsInstance)).toBe(__DEV__);
73+
});
74+
});

0 commit comments

Comments
 (0)