Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
import type {ColorValue} from '../../StyleSheet/StyleSheet';
import type {ViewProps} from '../View/ViewPropTypes';

import useMergeRefs from '../../Utilities/useMergeRefs';
import AndroidSwipeRefreshLayoutNativeComponent, {
Commands as AndroidSwipeRefreshLayoutCommands,
} from './AndroidSwipeRefreshLayoutNativeComponent';
import PullToRefreshViewNativeComponent, {
Commands as PullToRefreshCommands,
} from './PullToRefreshViewNativeComponent';
import * as React from 'react';
import {useCallback, useEffect, useRef, useState} from 'react';

const Platform = require('../../Utilities/Platform').default;

Expand Down Expand Up @@ -94,14 +96,18 @@ type RefreshControlBaseProps = Readonly<{
progressViewOffset?: ?number,
}>;

/** @build-types emit-as-interface Uniwind compatibility */
export type RefreshControlProps = Readonly<{
...ViewProps,
...RefreshControlPropsIOS,
...RefreshControlPropsAndroid,
...RefreshControlBaseProps,
}>;

export type RefreshControlInstance = React.ElementRef<
| typeof PullToRefreshViewNativeComponent
| typeof AndroidSwipeRefreshLayoutNativeComponent,
>;

/**
* Used inside a `ScrollView` to add pull to refresh functionality. When the
* `ScrollView` is at `scrollY: 0`, swiping down triggers an `onRefresh` event.
Expand All @@ -112,86 +118,100 @@ export type RefreshControlProps = Readonly<{
*
* @see https://reactnative.dev/docs/refreshcontrol
*/
class RefreshControl extends React.Component<RefreshControlProps> {
_nativeRef: ?React.ElementRef<
| typeof PullToRefreshViewNativeComponent
| typeof AndroidSwipeRefreshLayoutNativeComponent,
>;
_lastNativeRefreshing: boolean = false;

componentDidMount() {
this._lastNativeRefreshing = this.props.refreshing;
}
const RefreshControl: component(
ref?: React.RefSetter<RefreshControlInstance>,
...props: RefreshControlProps
) = ({
// Android only props
enabled,
colors,
progressBackgroundColor,
size,
// iOS only props
tintColor,
titleColor,
title,
// Common props
onRefresh,
refreshing,
ref: forwardedRef,
...viewProps
}: {
ref?: React.RefSetter<RefreshControlInstance>,
...RefreshControlProps,
}): React.Node => {
const localRef = useRef<RefreshControlInstance | null>(null);
const ref = useMergeRefs(localRef, forwardedRef);

const [rerender, forceRerender] = useState(0);
const nativeRefreshingState = useRef(refreshing);

const handleRefresh = useCallback(() => {
nativeRefreshingState.current = true; // Native state has changed to `true` on `onRefresh` callback.

// $FlowFixMe[unused-promise]
onRefresh?.();

componentDidUpdate(prevProps: RefreshControlProps) {
// RefreshControl is a controlled component so if the native refreshing
// value doesn't match the current js refreshing prop update it to
// the js value.
if (this.props.refreshing !== prevProps.refreshing) {
this._lastNativeRefreshing = this.props.refreshing;
} else if (
this.props.refreshing !== this._lastNativeRefreshing &&
this._nativeRef
) {
if (Platform.OS === 'android') {
AndroidSwipeRefreshLayoutCommands.setNativeRefreshing(
this._nativeRef,
this.props.refreshing,
);
} else {
PullToRefreshCommands.setNativeRefreshing(
this._nativeRef,
this.props.refreshing,
);
}
this._lastNativeRefreshing = this.props.refreshing;
// The native component will start refreshing so force an update to
// make sure it stays in sync with the js component.
forceRerender(val => val + 1);
}, [onRefresh]);

// RefreshControl is a controlled component so if the native refreshing
// value doesn't match the current js refreshing prop update it to
// the js value.
useEffect(() => {
const viewRef = localRef.current;
if (!viewRef) {
return;
}

// Do nothing when a native state is the same as a `refreshing` prop
if (nativeRefreshingState.current === refreshing) {
return;
}
}

render(): React.Node {
if (Platform.OS === 'ios') {
const {enabled, colors, progressBackgroundColor, size, ...props} =
this.props;
return (
<PullToRefreshViewNativeComponent
{...props}
ref={this._setNativeRef}
onRefresh={this._onRefresh}
/>
// Otherwise a JS component has to sync a native state with an actual `refreshing` value
if (Platform.OS === 'android') {
AndroidSwipeRefreshLayoutCommands.setNativeRefreshing(
viewRef,
refreshing,
);
} else {
const {tintColor, titleColor, title, ...props} = this.props;
return (
<AndroidSwipeRefreshLayoutNativeComponent
{...props}
ref={this._setNativeRef}
onRefresh={this._onRefresh}
/>
);
PullToRefreshCommands.setNativeRefreshing(viewRef, refreshing);
}
}

_onRefresh = () => {
this._lastNativeRefreshing = true;

// $FlowFixMe[unused-promise]
this.props.onRefresh && this.props.onRefresh();
nativeRefreshingState.current = refreshing;
}, [refreshing, rerender]);

if (Platform.OS === 'ios') {
return (
<PullToRefreshViewNativeComponent
{...viewProps}
refreshing={refreshing}
tintColor={tintColor}
titleColor={titleColor}
title={title}
ref={ref}
onRefresh={handleRefresh}
/>
);
}

// The native component will start refreshing so force an update to
// make sure it stays in sync with the js component.
this.forceUpdate();
};

_setNativeRef = (
ref: ?React.ElementRef<
| typeof PullToRefreshViewNativeComponent
| typeof AndroidSwipeRefreshLayoutNativeComponent,
>,
) => {
this._nativeRef = ref;
};
}

export type RefreshControlInstance = RefreshControl;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RefreshControlInstance is not a class instance anymore!

ref is forwarded down to native components (PullToRefreshViewNativeComponent or AndroidSwipeRefreshLayoutNativeComponent)

return (
<AndroidSwipeRefreshLayoutNativeComponent
{...viewProps}
refreshing={refreshing}
enabled={enabled}
colors={colors}
progressBackgroundColor={progressBackgroundColor}
size={size}
ref={ref}
onRefresh={handleRefresh}
/>
);
};

RefreshControl.displayName = 'RefreshControl';

export default RefreshControl;
60 changes: 48 additions & 12 deletions packages/react-native/ReactNativeApi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<cf6a759b9ae1a69151a61e1babd1abd3>>
* @generated SignedSource<<446b2576ba542a7b3029ac187fe61fcc>>
*
* This file was generated by scripts/js-api/build-types/index.js.
*/
Expand All @@ -22,6 +22,7 @@
/* eslint-disable redundant-undefined/redundant-undefined */

import * as React from "react"
declare const $$AndroidSwipeRefreshLayoutNativeComponent: NativeType
declare const $$AnimatedFlatList: typeof AnimatedFlatList_default
declare const $$AnimatedImage: AnimatedComponentType<
React.JSX.LibraryManagedAttributes<
Expand Down Expand Up @@ -77,6 +78,8 @@ declare const $$index: {
get VirtualizedSectionList(): AnyVirtualizedSectionList
}
declare const $$NativeDeviceInfo: typeof NativeModule_default
declare const $$PullToRefreshViewNativeComponent: typeof $$PullToRefreshViewNativeComponent_2
declare const $$PullToRefreshViewNativeComponent_2: HostComponent<PullToRefreshNativeProps>
declare const $$ReactFabric: typeof ReactFabric_default
declare const $$ScrollViewContext: typeof ScrollViewContext_default
declare const absoluteFill: AbsoluteFillStyle
Expand Down Expand Up @@ -348,6 +351,12 @@ declare const RCTNetworking_default: {
}
declare let ReactFabric_default: ReactFabricType
declare const ReadOnlyNodeBase: typeof Object
declare const RefreshControl: typeof RefreshControl_default
declare const RefreshControl_default: (
props: RefreshControlProps & {
ref?: React.Ref<RefreshControlInstance>
},
) => React.ReactNode
declare const registerCallableModule: typeof registerCallableModule_default
declare const registerCallableModule_default: RegisterCallableModule
declare const requireNativeComponent: typeof requireNativeComponent_default
Expand Down Expand Up @@ -915,6 +924,8 @@ declare class _TextInputInstance extends ReactNativeElement_default {
isFocused(): boolean
setSelection(start: number, end: number): void
}
declare type $$AndroidSwipeRefreshLayoutNativeComponent =
typeof $$AndroidSwipeRefreshLayoutNativeComponent
declare type $$AnimatedFlatList = typeof $$AnimatedFlatList
declare type $$AnimatedImage = typeof $$AnimatedImage
declare type $$AnimatedImplementation = typeof $$AnimatedImplementation
Expand All @@ -925,6 +936,10 @@ declare type $$AnimatedView = typeof $$AnimatedView
declare type $$flattenStyle = typeof $$flattenStyle
declare type $$index = typeof $$index
declare type $$NativeDeviceInfo = typeof $$NativeDeviceInfo
declare type $$PullToRefreshViewNativeComponent =
typeof $$PullToRefreshViewNativeComponent
declare type $$PullToRefreshViewNativeComponent_2 =
typeof $$PullToRefreshViewNativeComponent_2
declare type $$ReactFabric = typeof $$ReactFabric
declare type $$ScrollViewContext = typeof $$ScrollViewContext
declare type absoluteFill = typeof absoluteFill
Expand Down Expand Up @@ -1200,6 +1215,17 @@ declare type AndroidProps = {
readonly nextFocusRight?: number
readonly nextFocusUp?: number
}
declare type AndroidSwipeRefreshLayoutNativeProps = Readonly<
ViewProps & {
colors?: ReadonlyArray<ColorValue>
enabled?: WithDefault<boolean, true>
onRefresh?: DirectEventHandler<null>
progressBackgroundColor?: ColorValue
progressViewOffset?: WithDefault<Float, 0>
refreshing: boolean
size?: WithDefault<"default" | "large", "default">
}
>
declare namespace Animated {
export {
CompositeAnimation,
Expand Down Expand Up @@ -3233,6 +3259,7 @@ declare type NativeTouchEvent = {
readonly timestamp: number
readonly touches: ReadonlyArray<NativeTouchEvent>
}
declare type NativeType = HostComponent<AndroidSwipeRefreshLayoutNativeProps>
declare interface NativeUIEvent {
readonly detail: number
}
Expand Down Expand Up @@ -3704,6 +3731,16 @@ declare type PublicRootInstance = symbol & {
__PublicRootInstance__: string
}
declare type PublicTextInstance = ReturnType<createPublicTextInstanceT>
declare type PullToRefreshNativeProps = Readonly<
ViewProps & {
onRefresh?: DirectEventHandler<null>
progressViewOffset?: WithDefault<Float, 0>
refreshing: boolean
tintColor?: ColorValue
title?: WithDefault<string, null>
titleColor?: ColorValue
}
>
declare interface PushNotification {
finish(result: string): void
getAlert(): (Object | undefined) | (string | undefined)
Expand Down Expand Up @@ -4001,23 +4038,22 @@ declare type Rect = {
readonly top?: number
}
declare type RectOrSize = number | Rect
declare class RefreshControl extends React.Component<RefreshControlProps> {
componentDidMount(): void
componentDidUpdate(prevProps: RefreshControlProps): void
render(): React.ReactNode
}
declare type RefreshControl = typeof RefreshControl
declare type RefreshControlBaseProps = {
readonly onRefresh?: () => Promise<void> | void
readonly progressViewOffset?: number
readonly refreshing: boolean
}
declare type RefreshControlInstance = RefreshControl
declare interface RefreshControlProps extends Readonly<
declare type RefreshControlInstance = React.ComponentRef<
| typeof $$AndroidSwipeRefreshLayoutNativeComponent
| typeof $$PullToRefreshViewNativeComponent
>
declare type RefreshControlProps = Readonly<
ViewProps &
RefreshControlPropsIOS &
RefreshControlPropsAndroid &
RefreshControlBaseProps
> {}
>
declare type RefreshControlPropsAndroid = {
readonly colors?: ReadonlyArray<ColorValue>
readonly enabled?: boolean
Expand Down Expand Up @@ -5931,9 +5967,9 @@ export {
PushNotificationPermissions, // c2e7ae4f
Rationale, // 5df1b1c1
ReactNativeVersion, // abd76827
RefreshControl, // 1fc5c032
RefreshControlInstance, // 7cef228c
RefreshControlProps, // c03c6dd3
RefreshControl, // 075bafb2
RefreshControlInstance, // 39a2f202
RefreshControlProps, // e57c3896
RefreshControlPropsAndroid, // 8ac931ca
RefreshControlPropsIOS, // 72a36381
Registry, // 6c39216d
Expand Down
Loading