diff --git a/packages/react-native/Libraries/Components/RefreshControl/RefreshControl.js b/packages/react-native/Libraries/Components/RefreshControl/RefreshControl.js index 437a83b3f1eb..35714b18d28c 100644 --- a/packages/react-native/Libraries/Components/RefreshControl/RefreshControl.js +++ b/packages/react-native/Libraries/Components/RefreshControl/RefreshControl.js @@ -11,6 +11,7 @@ import type {ColorValue} from '../../StyleSheet/StyleSheet'; import type {ViewProps} from '../View/ViewPropTypes'; +import useMergeRefs from '../../Utilities/useMergeRefs'; import AndroidSwipeRefreshLayoutNativeComponent, { Commands as AndroidSwipeRefreshLayoutCommands, } from './AndroidSwipeRefreshLayoutNativeComponent'; @@ -18,6 +19,7 @@ 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; @@ -94,7 +96,6 @@ type RefreshControlBaseProps = Readonly<{ progressViewOffset?: ?number, }>; -/** @build-types emit-as-interface Uniwind compatibility */ export type RefreshControlProps = Readonly<{ ...ViewProps, ...RefreshControlPropsIOS, @@ -102,6 +103,11 @@ export type RefreshControlProps = Readonly<{ ...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. @@ -112,86 +118,100 @@ export type RefreshControlProps = Readonly<{ * * @see https://reactnative.dev/docs/refreshcontrol */ -class RefreshControl extends React.Component { - _nativeRef: ?React.ElementRef< - | typeof PullToRefreshViewNativeComponent - | typeof AndroidSwipeRefreshLayoutNativeComponent, - >; - _lastNativeRefreshing: boolean = false; - - componentDidMount() { - this._lastNativeRefreshing = this.props.refreshing; - } +const RefreshControl: component( + ref?: React.RefSetter, + ...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, + ...RefreshControlProps, +}): React.Node => { + const localRef = useRef(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 ( - + // 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 ( - - ); + 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 ( + + ); + } - // 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; + return ( + + ); +}; + +RefreshControl.displayName = 'RefreshControl'; export default RefreshControl; diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index e1654b681c24..4bf72d968233 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -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<> + * @generated SignedSource<<446b2576ba542a7b3029ac187fe61fcc>> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -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< @@ -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 declare const $$ReactFabric: typeof ReactFabric_default declare const $$ScrollViewContext: typeof ScrollViewContext_default declare const absoluteFill: AbsoluteFillStyle @@ -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 + }, +) => React.ReactNode declare const registerCallableModule: typeof registerCallableModule_default declare const registerCallableModule_default: RegisterCallableModule declare const requireNativeComponent: typeof requireNativeComponent_default @@ -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 @@ -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 @@ -1200,6 +1215,17 @@ declare type AndroidProps = { readonly nextFocusRight?: number readonly nextFocusUp?: number } +declare type AndroidSwipeRefreshLayoutNativeProps = Readonly< + ViewProps & { + colors?: ReadonlyArray + enabled?: WithDefault + onRefresh?: DirectEventHandler + progressBackgroundColor?: ColorValue + progressViewOffset?: WithDefault + refreshing: boolean + size?: WithDefault<"default" | "large", "default"> + } +> declare namespace Animated { export { CompositeAnimation, @@ -3233,6 +3259,7 @@ declare type NativeTouchEvent = { readonly timestamp: number readonly touches: ReadonlyArray } +declare type NativeType = HostComponent declare interface NativeUIEvent { readonly detail: number } @@ -3704,6 +3731,16 @@ declare type PublicRootInstance = symbol & { __PublicRootInstance__: string } declare type PublicTextInstance = ReturnType +declare type PullToRefreshNativeProps = Readonly< + ViewProps & { + onRefresh?: DirectEventHandler + progressViewOffset?: WithDefault + refreshing: boolean + tintColor?: ColorValue + title?: WithDefault + titleColor?: ColorValue + } +> declare interface PushNotification { finish(result: string): void getAlert(): (Object | undefined) | (string | undefined) @@ -4001,23 +4038,22 @@ declare type Rect = { readonly top?: number } declare type RectOrSize = number | Rect -declare class RefreshControl extends React.Component { - componentDidMount(): void - componentDidUpdate(prevProps: RefreshControlProps): void - render(): React.ReactNode -} +declare type RefreshControl = typeof RefreshControl declare type RefreshControlBaseProps = { readonly onRefresh?: () => Promise | 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 readonly enabled?: boolean @@ -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