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
Dan Abramov committed
Apr 26, 2018 at 19:47 UTC
d883d59863483a1fb9f3dd0455022e4c6e1a41d5
2 files changed
+46
-5
packages/react-reconciler/src/ReactFiberBeginWork.js
+13
-5
@@ -169,12 +169,20 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
169
170
function updateForwardRef(current, workInProgress) {
171
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);
184
reconcileChildren(current, workInProgress, nextChildren);
177
- memoizeProps(workInProgress, nextChildren);
185
+ memoizeProps(workInProgress, nextProps);
186
return workInProgress.child;
187
}
188
packages/react/src/__tests__/forwardRef-test.internal.js
+33
@@ -232,6 +232,39 @@ describe('forwardRef', () => {
232
expect(ref.current).toBe(null);
233
});
234
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
+
268
it('should warn if not provided a callback during creation', () => {
269
expect(() => React.forwardRef(undefined)).toWarnDev(
270
'forwardRef requires a render function but was given undefined.',