Skip to content

Commit 928a2be

Browse files
jackpopeJack Popeacdlite
authored andcommitted
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 <[email protected]> Co-authored-by: Andrew Clark <[email protected]>
1 parent e2844c2 commit 928a2be

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

packages/react/src/jsx/ReactJSXElement.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -1149,7 +1149,15 @@ function coerceStringRef(mixedRef, owner, type) {
11491149
}
11501150
}
11511151

1152-
return stringRefAsCallbackRef.bind(null, stringRef, type, owner);
1152+
const callback = stringRefAsCallbackRef.bind(null, stringRef, type, owner);
1153+
// This is used to check whether two callback refs conceptually represent
1154+
// the same string ref, and can therefore be reused by the reconciler. Needed
1155+
// for backwards compatibility with old Meta code that relies on string refs
1156+
// not being reattached on every render.
1157+
callback.__stringRef = stringRef;
1158+
callback.__type = type;
1159+
callback.__owner = owner;
1160+
return callback;
11531161
}
11541162

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

0 commit comments

Comments
 (0)