Skip to content

Commit d2f314a

Browse files
kaciebfacebook-github-bot
authored andcommitted
Make ScrollView use ForwardRef
Summary: Have ScrollView use forwardRef so that the host component methods like `measure` and `measureLayout` are available without having to call `getNativeScrollRef`. Instead, you can use `<ScrollView ref={myRef} />` and directly call all methods of ScrollView and host components on `myRef`. Previous usage: ``` const myRef = React.createRef<React.ElementRef<typeof ScrollView>>(); <ScrollView ref={myRef} /> const innerViewRef = myRef.current.getNativeScrollRef(); innerViewRef.measure(); ``` New usage: ``` const myRef = React.createRef<React.ElementRef<typeof View>>(); <ScrollView ref={myRef} /> // now, myRef.current can be used directly as the ref myRef.current.measure(); myRef.current.measureLayout(); // Additionally, myRef still has access to ScrollView methods myRef.current.scrollTo(...); ``` Changes: * Added deprecation warnings to ScrollView methods `getNativeScrollRef`, `getScrollableNode`, and `getScrollResponder` * Added the forwardRef call to create `ForwardedScrollView` - this takes in `ref` and passes it into the class ScrollView as `scrollViewRef`. * Forwarded the ref to the native scroll view using `setAndForwardRef`. * Added statics onto `ForwardedScrollView` so that `ScrollView.Context` can still be accessed. * Added type `ScrollViewImperativeMethods`, which lists the public methods of ScrollView. * Converted all public methods of ScrollView to arrow functions. This is because they need to be bound to the forwarded ref. * Bound all public methods of ScrollView to the forwarded ref in the `setAndForwardRef` call. * Flow typed the final output (ForwardedScrollView) as an abstract component that takes in the props of the `ScrollView` class, and has all methods of both the inner host component (`measure`, `measureLayout`, etc) and the public methods (`scrollTo`, etc). Changes to mockScrollView: * Changed mockScrollView to be able to mock the function component instead of a class component * Updated necessary tests Changelog: [General] [Changed] - Make ScrollView use forwardRef Reviewed By: TheSavior Differential Revision: D19304480 fbshipit-source-id: 6c359897526d9d5ac6bc6ab6d5f9d82bfc0d8af4
1 parent 93ee5b2 commit d2f314a

File tree

10 files changed

+211
-102
lines changed

10 files changed

+211
-102
lines changed

Libraries/Components/ScrollView/ScrollView.js

+126-32
Original file line numberDiff line numberDiff line change
@@ -62,20 +62,29 @@ if (Platform.OS === 'android') {
6262
RCTScrollContentView = ScrollContentViewNativeComponent;
6363
}
6464

65-
export type ScrollResponderType = {
66-
// We'd like to do ...ScrollView here, however Flow doesn't seem
67-
// to see the imperative methods of ScrollView that way. Workaround the
68-
// issue by specifying them manually.
65+
// Public methods for ScrollView
66+
export type ScrollViewImperativeMethods = $ReadOnly<{|
67+
getScrollResponder: $PropertyType<ScrollView, 'getScrollResponder'>,
6968
getScrollableNode: $PropertyType<ScrollView, 'getScrollableNode'>,
7069
getInnerViewNode: $PropertyType<ScrollView, 'getInnerViewNode'>,
7170
getInnerViewRef: $PropertyType<ScrollView, 'getInnerViewRef'>,
7271
getNativeScrollRef: $PropertyType<ScrollView, 'getNativeScrollRef'>,
73-
setNativeProps: $PropertyType<ScrollView, 'setNativeProps'>,
7472
scrollTo: $PropertyType<ScrollView, 'scrollTo'>,
73+
scrollToEnd: $PropertyType<ScrollView, 'scrollToEnd'>,
7574
flashScrollIndicators: $PropertyType<ScrollView, 'flashScrollIndicators'>,
76-
...typeof ScrollResponder.Mixin,
77-
...
78-
};
75+
76+
// ScrollResponder.Mixin public methods
77+
scrollResponderZoomTo: $PropertyType<
78+
typeof ScrollResponder.Mixin,
79+
'scrollResponderZoomTo',
80+
>,
81+
scrollResponderScrollNativeHandleToKeyboard: $PropertyType<
82+
typeof ScrollResponder.Mixin,
83+
'scrollResponderScrollNativeHandleToKeyboard',
84+
>,
85+
|}>;
86+
87+
export type ScrollResponderType = ScrollViewImperativeMethods;
7988

8089
type IOSProps = $ReadOnly<{|
8190
/**
@@ -581,6 +590,14 @@ export type Props = $ReadOnly<{|
581590
* instead of calling `getInnerViewRef`.
582591
*/
583592
innerViewRef?: React.Ref<typeof View>,
593+
/**
594+
* A ref to the Native ScrollView component. This ref can be used to call
595+
* all of ScrollView's public methods, in addition to native methods like
596+
* measure, measureLayout, etc.
597+
*/
598+
scrollViewRef?: React.Ref<
599+
typeof ScrollViewNativeComponent & ScrollViewImperativeMethods,
600+
>,
584601
|}>;
585602

586603
type State = {|
@@ -603,11 +620,14 @@ function createScrollResponder(
603620
}
604621

605622
type ContextType = {|horizontal: boolean|} | null;
606-
const Context = React.createContext<ContextType>(null);
623+
const Context: React.Context<ContextType> = React.createContext(null);
607624
const standardHorizontalContext: ContextType = Object.freeze({
608625
horizontal: true,
609626
});
610627
const standardVerticalContext: ContextType = Object.freeze({horizontal: false});
628+
type ScrollViewComponentStatics = $ReadOnly<{|
629+
Context: typeof Context,
630+
|}>;
611631

612632
/**
613633
* Component that wraps platform ScrollView while providing
@@ -750,24 +770,64 @@ class ScrollView extends React.Component<Props, State> {
750770
}
751771
}
752772

753-
setNativeProps(props: {[key: string]: mixed, ...}) {
754-
this._scrollViewRef && this._scrollViewRef.setNativeProps(props);
755-
}
773+
_setNativeRef = setAndForwardRef({
774+
getForwardedRef: () => this.props.scrollViewRef,
775+
setLocalRef: ref => {
776+
this._scrollViewRef = ref;
777+
778+
/*
779+
This is a hack. Ideally we would forwardRef to the underlying
780+
host component. However, since ScrollView has it's own methods that can be
781+
called as well, if we used the standard forwardRef then these
782+
methods wouldn't be accessible and thus be a breaking change.
783+
784+
Therefore we edit ref to include ScrollView's public methods so that
785+
they are callable from the ref.
786+
*/
787+
if (ref) {
788+
ref.getScrollResponder = this.getScrollResponder;
789+
ref.getScrollableNode = this.getScrollableNode;
790+
ref.getInnerViewNode = this.getInnerViewNode;
791+
ref.getInnerViewRef = this.getInnerViewRef;
792+
ref.getNativeScrollRef = this.getNativeScrollRef;
793+
ref.scrollTo = this.scrollTo;
794+
ref.scrollToEnd = this.scrollToEnd;
795+
ref.flashScrollIndicators = this.flashScrollIndicators;
796+
797+
// $FlowFixMe - This method was manually bound from ScrollResponder.mixin
798+
ref.scrollResponderZoomTo = this.scrollResponderZoomTo;
799+
// $FlowFixMe - This method was manually bound from ScrollResponder.mixin
800+
ref.scrollResponderScrollNativeHandleToKeyboard = this.scrollResponderScrollNativeHandleToKeyboard;
801+
}
802+
},
803+
});
756804

757805
/**
758806
* Returns a reference to the underlying scroll responder, which supports
759807
* operations like `scrollTo`. All ScrollView-like components should
760808
* implement this method so that they can be composed while providing access
761809
* to the underlying scroll responder's methods.
762810
*/
763-
getScrollResponder(): ScrollResponderType {
811+
getScrollResponder: () => ScrollResponderType = () => {
812+
if (__DEV__) {
813+
console.warn(
814+
'`getScrollResponder()` is deprecated. This will be removed in a future release. ' +
815+
'Use <ScrollView ref={myRef} /> instead.',
816+
);
817+
}
764818
// $FlowFixMe - overriding type to include ScrollResponder.Mixin
765819
return ((this: any): ScrollResponderType);
766-
}
820+
};
767821

768-
getScrollableNode(): ?number {
822+
getScrollableNode: () => ?number = () => {
823+
if (__DEV__) {
824+
console.warn(
825+
'`getScrollableNode()` is deprecated. This will be removed in a future release. ' +
826+
'Use <ScrollView ref={myRef} /> instead.',
827+
);
828+
}
769829
return ReactNative.findNodeHandle(this._scrollViewRef);
770-
}
830+
};
771831

772832
getInnerViewNode(): ?number {
773833
console.warn(
@@ -785,9 +845,15 @@ class ScrollView extends React.Component<Props, State> {
785845
return this._innerViewRef;
786846
}
787847

788-
getNativeScrollRef(): ?React.ElementRef<HostComponent<mixed>> {
848+
getNativeScrollRef: () => ?React.ElementRef<HostComponent<mixed>> = () => {
849+
if (__DEV__) {
850+
console.warn(
851+
'`getNativeScrollRef()` is deprecated. This will be removed in a future release. ' +
852+
'Use <ScrollView ref={myRef} /> instead.',
853+
);
854+
}
789855
return this._scrollViewRef;
790-
}
856+
};
791857

792858
/**
793859
* Scrolls to a given x, y offset, either immediately or with a smooth animation.
@@ -800,7 +866,7 @@ class ScrollView extends React.Component<Props, State> {
800866
* the function also accepts separate arguments as an alternative to the options object.
801867
* This is deprecated due to ambiguity (y before x), and SHOULD NOT BE USED.
802868
*/
803-
scrollTo(
869+
scrollTo: (
804870
options?:
805871
| {
806872
x?: number,
@@ -811,7 +877,18 @@ class ScrollView extends React.Component<Props, State> {
811877
| number,
812878
deprecatedX?: number,
813879
deprecatedAnimated?: boolean,
814-
) {
880+
) => void = (
881+
options?:
882+
| {
883+
x?: number,
884+
y?: number,
885+
animated?: boolean,
886+
...
887+
}
888+
| number,
889+
deprecatedX?: number,
890+
deprecatedAnimated?: boolean,
891+
) => {
815892
let x, y, animated;
816893
if (typeof options === 'number') {
817894
console.warn(
@@ -831,7 +908,7 @@ class ScrollView extends React.Component<Props, State> {
831908
y: y || 0,
832909
animated: animated !== false,
833910
});
834-
}
911+
};
835912

836913
/**
837914
* If this is a vertical ScrollView scrolls to the bottom.
@@ -841,22 +918,24 @@ class ScrollView extends React.Component<Props, State> {
841918
* `scrollToEnd({animated: false})` for immediate scrolling.
842919
* If no options are passed, `animated` defaults to true.
843920
*/
844-
scrollToEnd(options?: ?{animated?: boolean, ...}) {
921+
scrollToEnd: (options?: ?{animated?: boolean, ...}) => void = (
922+
options?: ?{animated?: boolean, ...},
923+
) => {
845924
// Default to true
846925
const animated = (options && options.animated) !== false;
847926
this._scrollResponder.scrollResponderScrollToEnd({
848927
animated: animated,
849928
});
850-
}
929+
};
851930

852931
/**
853932
* Displays the scroll indicators momentarily.
854933
*
855934
* @platform ios
856935
*/
857-
flashScrollIndicators() {
936+
flashScrollIndicators: () => void = () => {
858937
this._scrollResponder.scrollResponderFlashScrollIndicators();
859-
}
938+
};
860939

861940
_getKeyForIndex(index, childArray) {
862941
const child = childArray[index];
@@ -959,9 +1038,6 @@ class ScrollView extends React.Component<Props, State> {
9591038
};
9601039

9611040
_scrollViewRef: ?React.ElementRef<HostComponent<mixed>> = null;
962-
_setScrollViewRef = (ref: ?React.ElementRef<HostComponent<mixed>>) => {
963-
this._scrollViewRef = ref;
964-
};
9651041

9661042
_innerViewRef: ?React.ElementRef<typeof View> = null;
9671043
_setInnerViewRef = setAndForwardRef({
@@ -1182,7 +1258,7 @@ class ScrollView extends React.Component<Props, State> {
11821258
/* $FlowFixMe(>=0.117.0 site=react_native_fb) This comment suppresses
11831259
* an error found when Flow v0.117 was deployed. To see the error,
11841260
* delete this comment and run Flow. */
1185-
<ScrollViewClass {...props} ref={this._setScrollViewRef}>
1261+
<ScrollViewClass {...props} ref={this._setNativeRef}>
11861262
{Platform.isTV ? null : refreshControl}
11871263
{contentContainer}
11881264
</ScrollViewClass>
@@ -1200,14 +1276,14 @@ class ScrollView extends React.Component<Props, State> {
12001276
<ScrollViewClass
12011277
{...props}
12021278
style={[baseStyle, inner]}
1203-
ref={this._setScrollViewRef}>
1279+
ref={this._setNativeRef}>
12041280
{contentContainer}
12051281
</ScrollViewClass>,
12061282
);
12071283
}
12081284
}
12091285
return (
1210-
<ScrollViewClass {...props} ref={this._setScrollViewRef}>
1286+
<ScrollViewClass {...props} ref={this._setNativeRef}>
12111287
{contentContainer}
12121288
</ScrollViewClass>
12131289
);
@@ -1232,4 +1308,22 @@ const styles = StyleSheet.create({
12321308
},
12331309
});
12341310

1235-
module.exports = ScrollView;
1311+
function Wrapper(props, ref) {
1312+
return <ScrollView {...props} scrollViewRef={ref} />;
1313+
}
1314+
Wrapper.displayName = 'ScrollView';
1315+
const ForwardedScrollView = React.forwardRef(Wrapper);
1316+
1317+
// $FlowFixMe Add static context to ForwardedScrollView
1318+
ForwardedScrollView.Context = Context;
1319+
1320+
ForwardedScrollView.displayName = 'ScrollView';
1321+
1322+
module.exports = ((ForwardedScrollView: $FlowFixMe): React.AbstractComponent<
1323+
React.ElementConfig<typeof ScrollView>,
1324+
$ReadOnly<{|
1325+
...$Exact<React.ElementRef<HostComponent<mixed>>>,
1326+
...ScrollViewImperativeMethods,
1327+
|}>,
1328+
> &
1329+
ScrollViewComponentStatics);

Libraries/Components/ScrollView/__mocks__/ScrollViewMock.js

-39
This file was deleted.

Libraries/Components/ScrollView/__tests__/ScrollView-test.js

+16-1
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66
*
77
* @format
88
* @emails oncall+react_native
9-
* @flow strict-local
9+
* @flow-strict
1010
*/
1111

1212
'use strict';
1313

1414
const React = require('react');
1515
const ScrollView = require('../ScrollView');
1616
const ReactNativeTestTools = require('../../../Utilities/ReactNativeTestTools');
17+
const ReactTestRenderer = require('react-test-renderer');
1718
const View = require('../../View/View');
1819
const Text = require('../../../Text/Text');
1920

@@ -33,4 +34,18 @@ describe('<ScrollView />', () => {
3334
},
3435
);
3536
});
37+
it('should mock native methods and instance methods when mocked', () => {
38+
jest.resetModules();
39+
jest.mock('../ScrollView');
40+
const ref = React.createRef();
41+
42+
ReactTestRenderer.create(<ScrollView ref={ref} />);
43+
44+
expect(ref.current != null && ref.current.measure).toBeInstanceOf(
45+
jest.fn().constructor,
46+
);
47+
expect(ref.current != null && ref.current.scrollTo).toBeInstanceOf(
48+
jest.fn().constructor,
49+
);
50+
});
3651
});

Libraries/Components/ScrollView/__tests__/__snapshots__/ScrollView-test.js.snap

+5-4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ exports[`<ScrollView /> should render as expected: should deep render when not m
3636
onTouchStart={[Function]}
3737
pagingEnabled={false}
3838
scrollBarThumbImage={null}
39+
scrollViewRef={null}
3940
sendMomentumEvents={false}
4041
snapToEnd={true}
4142
snapToStart={true}
@@ -70,21 +71,21 @@ exports[`<ScrollView /> should render as expected: should deep render when not m
7071
`;
7172

7273
exports[`<ScrollView /> should render as expected: should shallow render as <ScrollView /> when mocked 1`] = `
73-
<ScrollView>
74+
<ForwardRef(ScrollView)>
7475
<View>
7576
<Text>
7677
Hello World!
7778
</Text>
7879
</View>
79-
</ScrollView>
80+
</ForwardRef(ScrollView)>
8081
`;
8182

8283
exports[`<ScrollView /> should render as expected: should shallow render as <ScrollView /> when not mocked 1`] = `
83-
<ScrollView>
84+
<ForwardRef(ScrollView)>
8485
<View>
8586
<Text>
8687
Hello World!
8788
</Text>
8889
</View>
89-
</ScrollView>
90+
</ForwardRef(ScrollView)>
9091
`;

0 commit comments

Comments
 (0)