Fix `ReactFabricHostComponent` methods if detached (#21967)
Timothy Yung committed
Jul 26, 2021 at 21:02 UTC
d4d7864934025232e8bca5d2febec846ec1bd6e3
2 files changed
+201
-14
packages/react-native-renderer/src/ReactFabricHostConfig.js
+26
-14
@@ -137,17 +137,23 @@ class ReactFabricHostComponent {
137
}
138
139
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
+ }
147
}
148
149
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
+ }
157
}
158
159
measureLayout(
@@ -168,12 +174,18 @@ class ReactFabricHostComponent {
174
return;
175
}
176
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
+ }
189
}
190
191
setNativeProps(nativeProps: Object) {
packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js
+175
@@ -414,6 +414,37 @@ describe('ReactFabric', () => {
414
expect(successCallback).toHaveBeenCalledWith(10, 10, 100, 100, 0, 0);
415
});
416
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
+
448
it('should call FabricUIManager.measureInWindow on ref.measureInWindow', () => {
449
const View = createReactNativeComponentClass('RCTView', () => ({
450
validAttributes: {foo: true},
@@ -442,6 +473,37 @@ describe('ReactFabric', () => {
473
expect(successCallback).toHaveBeenCalledWith(10, 10, 100, 100);
474
});
475
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
+
507
it('should support ref in ref.measureLayout', () => {
508
const View = createReactNativeComponentClass('RCTView', () => ({
509
validAttributes: {foo: true},
@@ -480,6 +542,119 @@ describe('ReactFabric', () => {
542
expect(successCallback).toHaveBeenCalledWith(1, 1, 100, 100);
543
});
544
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
+
658
it('returns the correct instance and calls it in the callback', () => {
659
const View = createReactNativeComponentClass('RCTView', () => ({
660
validAttributes: {foo: true},