Skip to content

Commit d4d7864

Browse files
authored
Fix ReactFabricHostComponent methods if detached (#21967)
1 parent c21e8fc commit d4d7864

File tree

2 files changed

+201
-14
lines changed

2 files changed

+201
-14
lines changed

packages/react-native-renderer/src/ReactFabricHostConfig.js

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -137,17 +137,23 @@ class ReactFabricHostComponent {
137137
}
138138

139139
measure(callback: MeasureOnSuccessCallback) {
140-
fabricMeasure(
141-
this._internalInstanceHandle.stateNode.node,
142-
mountSafeCallback_NOT_REALLY_SAFE(this, callback),
143-
);
140+
const {stateNode} = this._internalInstanceHandle;
141+
if (stateNode != null) {
142+
fabricMeasure(
143+
stateNode.node,
144+
mountSafeCallback_NOT_REALLY_SAFE(this, callback),
145+
);
146+
}
144147
}
145148

146149
measureInWindow(callback: MeasureInWindowOnSuccessCallback) {
147-
fabricMeasureInWindow(
148-
this._internalInstanceHandle.stateNode.node,
149-
mountSafeCallback_NOT_REALLY_SAFE(this, callback),
150-
);
150+
const {stateNode} = this._internalInstanceHandle;
151+
if (stateNode != null) {
152+
fabricMeasureInWindow(
153+
stateNode.node,
154+
mountSafeCallback_NOT_REALLY_SAFE(this, callback),
155+
);
156+
}
151157
}
152158

153159
measureLayout(
@@ -168,12 +174,18 @@ class ReactFabricHostComponent {
168174
return;
169175
}
170176

171-
fabricMeasureLayout(
172-
this._internalInstanceHandle.stateNode.node,
173-
relativeToNativeNode._internalInstanceHandle.stateNode.node,
174-
mountSafeCallback_NOT_REALLY_SAFE(this, onFail),
175-
mountSafeCallback_NOT_REALLY_SAFE(this, onSuccess),
176-
);
177+
const toStateNode = this._internalInstanceHandle.stateNode;
178+
const fromStateNode =
179+
relativeToNativeNode._internalInstanceHandle.stateNode;
180+
181+
if (toStateNode != null && fromStateNode != null) {
182+
fabricMeasureLayout(
183+
toStateNode.node,
184+
fromStateNode.node,
185+
mountSafeCallback_NOT_REALLY_SAFE(this, onFail),
186+
mountSafeCallback_NOT_REALLY_SAFE(this, onSuccess),
187+
);
188+
}
177189
}
178190

179191
setNativeProps(nativeProps: Object) {

packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,37 @@ describe('ReactFabric', () => {
414414
expect(successCallback).toHaveBeenCalledWith(10, 10, 100, 100, 0, 0);
415415
});
416416

417+
it('should no-op if calling measure on unmounted refs', () => {
418+
const View = createReactNativeComponentClass('RCTView', () => ({
419+
validAttributes: {foo: true},
420+
uiViewClassName: 'RCTView',
421+
}));
422+
423+
nativeFabricUIManager.measure.mockClear();
424+
425+
let viewRef;
426+
act(() => {
427+
ReactFabric.render(
428+
<View
429+
ref={ref => {
430+
viewRef = ref;
431+
}}
432+
/>,
433+
11,
434+
);
435+
});
436+
const dangerouslyRetainedViewRef = viewRef;
437+
act(() => {
438+
ReactFabric.stopSurface(11);
439+
});
440+
441+
expect(nativeFabricUIManager.measure).not.toBeCalled();
442+
const successCallback = jest.fn();
443+
dangerouslyRetainedViewRef.measure(successCallback);
444+
expect(nativeFabricUIManager.measure).not.toBeCalled();
445+
expect(successCallback).not.toBeCalled();
446+
});
447+
417448
it('should call FabricUIManager.measureInWindow on ref.measureInWindow', () => {
418449
const View = createReactNativeComponentClass('RCTView', () => ({
419450
validAttributes: {foo: true},
@@ -442,6 +473,37 @@ describe('ReactFabric', () => {
442473
expect(successCallback).toHaveBeenCalledWith(10, 10, 100, 100);
443474
});
444475

476+
it('should no-op if calling measureInWindow on unmounted refs', () => {
477+
const View = createReactNativeComponentClass('RCTView', () => ({
478+
validAttributes: {foo: true},
479+
uiViewClassName: 'RCTView',
480+
}));
481+
482+
nativeFabricUIManager.measureInWindow.mockClear();
483+
484+
let viewRef;
485+
act(() => {
486+
ReactFabric.render(
487+
<View
488+
ref={ref => {
489+
viewRef = ref;
490+
}}
491+
/>,
492+
11,
493+
);
494+
});
495+
const dangerouslyRetainedViewRef = viewRef;
496+
act(() => {
497+
ReactFabric.stopSurface(11);
498+
});
499+
500+
expect(nativeFabricUIManager.measureInWindow).not.toBeCalled();
501+
const successCallback = jest.fn();
502+
dangerouslyRetainedViewRef.measureInWindow(successCallback);
503+
expect(nativeFabricUIManager.measureInWindow).not.toBeCalled();
504+
expect(successCallback).not.toBeCalled();
505+
});
506+
445507
it('should support ref in ref.measureLayout', () => {
446508
const View = createReactNativeComponentClass('RCTView', () => ({
447509
validAttributes: {foo: true},
@@ -480,6 +542,119 @@ describe('ReactFabric', () => {
480542
expect(successCallback).toHaveBeenCalledWith(1, 1, 100, 100);
481543
});
482544

545+
it('should no-op if calling measureLayout on unmounted "from" ref', () => {
546+
const View = createReactNativeComponentClass('RCTView', () => ({
547+
validAttributes: {foo: true},
548+
uiViewClassName: 'RCTView',
549+
}));
550+
551+
nativeFabricUIManager.measureLayout.mockClear();
552+
553+
let viewRef;
554+
let otherRef;
555+
act(() => {
556+
ReactFabric.render(
557+
<View>
558+
<View
559+
foo="bar"
560+
ref={ref => {
561+
viewRef = ref;
562+
}}
563+
/>
564+
<View
565+
ref={ref => {
566+
otherRef = ref;
567+
}}
568+
/>
569+
</View>,
570+
11,
571+
);
572+
});
573+
const dangerouslyRetainedOtherRef = otherRef;
574+
act(() => {
575+
ReactFabric.render(
576+
<View>
577+
<View
578+
foo="bar"
579+
ref={ref => {
580+
viewRef = ref;
581+
}}
582+
/>
583+
{null}
584+
</View>,
585+
11,
586+
);
587+
});
588+
589+
expect(nativeFabricUIManager.measureLayout).not.toBeCalled();
590+
const successCallback = jest.fn();
591+
const failureCallback = jest.fn();
592+
viewRef.measureLayout(
593+
dangerouslyRetainedOtherRef,
594+
successCallback,
595+
failureCallback,
596+
);
597+
expect(nativeFabricUIManager.measureLayout).not.toBeCalled();
598+
expect(successCallback).not.toBeCalled();
599+
expect(failureCallback).not.toBeCalled();
600+
});
601+
602+
it('should no-op if calling measureLayout on unmounted "to" ref', () => {
603+
const View = createReactNativeComponentClass('RCTView', () => ({
604+
validAttributes: {foo: true},
605+
uiViewClassName: 'RCTView',
606+
}));
607+
608+
nativeFabricUIManager.measureLayout.mockClear();
609+
610+
let viewRef;
611+
let otherRef;
612+
act(() => {
613+
ReactFabric.render(
614+
<View>
615+
<View
616+
foo="bar"
617+
ref={ref => {
618+
viewRef = ref;
619+
}}
620+
/>
621+
<View
622+
ref={ref => {
623+
otherRef = ref;
624+
}}
625+
/>
626+
</View>,
627+
11,
628+
);
629+
});
630+
const dangerouslyRetainedViewRef = viewRef;
631+
act(() => {
632+
ReactFabric.render(
633+
<View>
634+
{null}
635+
<View
636+
ref={ref => {
637+
otherRef = ref;
638+
}}
639+
/>
640+
</View>,
641+
11,
642+
);
643+
});
644+
645+
expect(nativeFabricUIManager.measureLayout).not.toBeCalled();
646+
const successCallback = jest.fn();
647+
const failureCallback = jest.fn();
648+
dangerouslyRetainedViewRef.measureLayout(
649+
otherRef,
650+
successCallback,
651+
failureCallback,
652+
);
653+
expect(nativeFabricUIManager.measureLayout).not.toBeCalled();
654+
expect(successCallback).not.toBeCalled();
655+
expect(failureCallback).not.toBeCalled();
656+
});
657+
483658
it('returns the correct instance and calls it in the callback', () => {
484659
const View = createReactNativeComponentClass('RCTView', () => ({
485660
validAttributes: {foo: true},

0 commit comments

Comments
 (0)