Created
December 3, 2023 21:52
-
-
Save PierreAndreis/3d3d47f3bd8ccde41ffd70659a4c9fb2 to your computer and use it in GitHub Desktop.
useScrollTop.ts expo-router
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copied from https://github.com/react-navigation/react-navigation/blob/main/packages/native/src/useScrollToTop.tsx | |
// and modified to work with expo-router | |
import { useNavigation } from "expo-router"; | |
import * as React from "react"; | |
import type { ScrollView } from "react-native"; | |
type ScrollOptions = { animated?: boolean; x?: number; y?: number }; | |
type ScrollableView = | |
| { scrollToTop(): void } | |
| { scrollTo(options: ScrollOptions): void } | |
| { scrollToOffset(options: { animated?: boolean; offset?: number }): void } | |
| { scrollResponderScrollTo(options: ScrollOptions): void }; | |
type ScrollableWrapper = | |
| { getScrollResponder(): React.ReactNode | ScrollView } | |
| { getNode(): ScrollableView } | |
| ScrollableView; | |
function getScrollableNode(ref: React.RefObject<ScrollableWrapper>) { | |
if (ref.current == null) { | |
return null; | |
} | |
if ( | |
"scrollToTop" in ref.current || | |
"scrollTo" in ref.current || | |
"scrollToOffset" in ref.current || | |
"scrollResponderScrollTo" in ref.current | |
) { | |
// This is already a scrollable node. | |
return ref.current; | |
} else if ("getScrollResponder" in ref.current) { | |
// If the view is a wrapper like FlatList, SectionList etc. | |
// We need to use `getScrollResponder` to get access to the scroll responder | |
return ref.current.getScrollResponder(); | |
} else if ("getNode" in ref.current) { | |
// When a `ScrollView` is wraped in `Animated.createAnimatedComponent` | |
// we need to use `getNode` to get the ref to the actual scrollview. | |
// Note that `getNode` is deprecated in newer versions of react-native | |
// this is why we check if we already have a scrollable node above. | |
return ref.current.getNode(); | |
} else { | |
return ref.current; | |
} | |
} | |
export function useScrollToTop(ref: React.RefObject<ScrollableWrapper>) { | |
const navigation = useNavigation(); | |
React.useEffect(() => { | |
const tabNavigations: (typeof navigation)[] = []; | |
let currentNavigation = navigation; | |
// If the screen is nested inside multiple tab navigators, we should scroll to top for any of them | |
// So we need to find all the parent tab navigators and add the listeners there | |
while (currentNavigation) { | |
if (currentNavigation.getState().type === "tab") { | |
tabNavigations.push(currentNavigation); | |
} | |
currentNavigation = currentNavigation.getParent(); | |
} | |
if (tabNavigations.length === 0) { | |
return; | |
} | |
const unsubscribers = tabNavigations.map((tab) => { | |
return tab.addListener("state", (state) => { | |
// We should scroll to top only when the screen is focused | |
const isFocused = navigation.isFocused(); | |
// In a nested stack navigator, tab press resets the stack to first screen | |
// So we should scroll to top only when we are on first screen | |
const isFirst = | |
tabNavigations.includes(navigation) || | |
state.data.state.routes[0]?.key === state.data.state.key; | |
// Run the operation in the next frame so we're sure all listeners have been run | |
// This is necessary to know if preventDefault() has been called | |
requestAnimationFrame(() => { | |
const scrollable = getScrollableNode(ref) as ScrollableWrapper; | |
if (isFocused && isFirst && scrollable) { | |
if ("scrollToTop" in scrollable) { | |
scrollable.scrollToTop(); | |
} else if ("scrollTo" in scrollable) { | |
scrollable.scrollTo({ animated: true, y: 0 }); | |
} else if ("scrollToOffset" in scrollable) { | |
scrollable.scrollToOffset({ animated: true, offset: 0 }); | |
} else if ("scrollResponderScrollTo" in scrollable) { | |
scrollable.scrollResponderScrollTo({ animated: true, y: 0 }); | |
} | |
} | |
}); | |
}); | |
}); | |
return () => { | |
unsubscribers.forEach((unsubscribe) => unsubscribe()); | |
}; | |
}, [navigation, ref]); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment