Skip to content

Commit d883d59

Browse files
authored
forwardRef() components should not re-render on deep setState() (#12690)
* Add a failing test for forwardRef memoization * Memoize forwardRef props and bail out on strict equality * Bail out only when ref matches the current ref
1 parent ec57d29 commit d883d59

File tree

2 files changed

+46
-5
lines changed

2 files changed

+46
-5
lines changed

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -169,12 +169,20 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
169169

170170
function updateForwardRef(current, workInProgress) {
171171
const render = workInProgress.type.render;
172-
const nextChildren = render(
173-
workInProgress.pendingProps,
174-
workInProgress.ref,
175-
);
172+
const nextProps = workInProgress.pendingProps;
173+
const ref = workInProgress.ref;
174+
if (hasLegacyContextChanged()) {
175+
// Normally we can bail out on props equality but if context has changed
176+
// we don't do the bailout and we have to reuse existing props instead.
177+
} else if (workInProgress.memoizedProps === nextProps) {
178+
const currentRef = current !== null ? current.ref : null;
179+
if (ref === currentRef) {
180+
return bailoutOnAlreadyFinishedWork(current, workInProgress);
181+
}
182+
}
183+
const nextChildren = render(nextProps, ref);
176184
reconcileChildren(current, workInProgress, nextChildren);
177-
memoizeProps(workInProgress, nextChildren);
185+
memoizeProps(workInProgress, nextProps);
178186
return workInProgress.child;
179187
}
180188

packages/react/src/__tests__/forwardRef-test.internal.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,39 @@ describe('forwardRef', () => {
232232
expect(ref.current).toBe(null);
233233
});
234234

235+
it('should not re-run the render callback on a deep setState', () => {
236+
let inst;
237+
238+
class Inner extends React.Component {
239+
render() {
240+
ReactNoop.yield('Inner');
241+
inst = this;
242+
return <div ref={this.props.forwardedRef} />;
243+
}
244+
}
245+
246+
function Middle(props) {
247+
ReactNoop.yield('Middle');
248+
return <Inner {...props} />;
249+
}
250+
251+
const Forward = React.forwardRef((props, ref) => {
252+
ReactNoop.yield('Forward');
253+
return <Middle {...props} forwardedRef={ref} />;
254+
});
255+
256+
function App() {
257+
ReactNoop.yield('App');
258+
return <Forward />;
259+
}
260+
261+
ReactNoop.render(<App />);
262+
expect(ReactNoop.flush()).toEqual(['App', 'Forward', 'Middle', 'Inner']);
263+
264+
inst.setState({});
265+
expect(ReactNoop.flush()).toEqual(['Inner']);
266+
});
267+
235268
it('should warn if not provided a callback during creation', () => {
236269
expect(() => React.forwardRef(undefined)).toWarnDev(
237270
'forwardRef requires a render function but was given undefined.',

0 commit comments

Comments
 (0)