Skip to content

Commit 4354159

Browse files
jackpopeJack Popeacdlite
authored
Backwards compatibility for string refs on WWW (#28826)
Seeing errors with undefined string ref values when trying to sync #28473 Added a test that reproduces the failing pattern. @acdlite pushed a786481 with fix --------- Co-authored-by: Jack Pope <jackpope@meta.com> Co-authored-by: Andrew Clark <git@andrewclark.io>
1 parent adb7173 commit 4354159

File tree

3 files changed

+77
-1
lines changed

3 files changed

+77
-1
lines changed

packages/react-dom/src/__tests__/ReactComponent-test.js

+49
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,55 @@ describe('ReactComponent', () => {
129129
}
130130
});
131131

132+
// @gate !disableStringRefs
133+
it('string refs do not detach and reattach on every render', async () => {
134+
spyOnDev(console, 'error').mockImplementation(() => {});
135+
136+
let refVal;
137+
class Child extends React.Component {
138+
componentDidUpdate() {
139+
// The parent ref should still be attached because it hasn't changed
140+
// since the last render. If the ref had changed, then this would be
141+
// undefined because refs are attached during the same phase (layout)
142+
// as componentDidUpdate, in child -> parent order. So the new parent
143+
// ref wouldn't have attached yet.
144+
refVal = this.props.contextRef();
145+
}
146+
147+
render() {
148+
if (this.props.show) {
149+
return <div>child</div>;
150+
}
151+
}
152+
}
153+
154+
class Parent extends React.Component {
155+
render() {
156+
return (
157+
<div id="test-root" ref="root">
158+
<Child
159+
contextRef={() => this.refs.root}
160+
show={this.props.showChild}
161+
/>
162+
</div>
163+
);
164+
}
165+
}
166+
167+
const container = document.createElement('div');
168+
const root = ReactDOMClient.createRoot(container);
169+
170+
await act(() => {
171+
root.render(<Parent />);
172+
});
173+
174+
expect(refVal).toBe(undefined);
175+
await act(() => {
176+
root.render(<Parent showChild={true} />);
177+
});
178+
expect(refVal).toBe(container.querySelector('#test-root'));
179+
});
180+
132181
// @gate !disableStringRefs
133182
it('should support string refs on owned components', async () => {
134183
const innerObj = {};

packages/react-reconciler/src/ReactFiberBeginWork.js

+19
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,25 @@ function markRef(current: Fiber | null, workInProgress: Fiber) {
10481048
);
10491049
}
10501050
if (current === null || current.ref !== ref) {
1051+
if (!disableStringRefs && current !== null) {
1052+
const oldRef = current.ref;
1053+
const newRef = ref;
1054+
if (
1055+
typeof oldRef === 'function' &&
1056+
typeof newRef === 'function' &&
1057+
typeof oldRef.__stringRef === 'string' &&
1058+
oldRef.__stringRef === newRef.__stringRef &&
1059+
oldRef.__stringRefType === newRef.__stringRefType &&
1060+
oldRef.__stringRefOwner === newRef.__stringRefOwner
1061+
) {
1062+
// Although this is a different callback, it represents the same
1063+
// string ref. To avoid breaking old Meta code that relies on string
1064+
// refs only being attached once, reuse the old ref. This will
1065+
// prevent us from detaching and reattaching the ref on each update.
1066+
workInProgress.ref = oldRef;
1067+
return;
1068+
}
1069+
}
10511070
// Schedule a Ref effect
10521071
workInProgress.flags |= Ref | RefStatic;
10531072
}

packages/react/src/jsx/ReactJSXElement.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -1189,7 +1189,15 @@ function coerceStringRef(mixedRef, owner, type) {
11891189
}
11901190
}
11911191

1192-
return stringRefAsCallbackRef.bind(null, stringRef, type, owner);
1192+
const callback = stringRefAsCallbackRef.bind(null, stringRef, type, owner);
1193+
// This is used to check whether two callback refs conceptually represent
1194+
// the same string ref, and can therefore be reused by the reconciler. Needed
1195+
// for backwards compatibility with old Meta code that relies on string refs
1196+
// not being reattached on every render.
1197+
callback.__stringRef = stringRef;
1198+
callback.__type = type;
1199+
callback.__owner = owner;
1200+
return callback;
11931201
}
11941202

11951203
function stringRefAsCallbackRef(stringRef, type, owner, value) {

0 commit comments

Comments
 (0)