Skip to content

Instantly share code, notes, and snippets.

@PierreAndreis
Created December 3, 2023 21:52
Show Gist options
  • Save PierreAndreis/3d3d47f3bd8ccde41ffd70659a4c9fb2 to your computer and use it in GitHub Desktop.
Save PierreAndreis/3d3d47f3bd8ccde41ffd70659a4c9fb2 to your computer and use it in GitHub Desktop.
useScrollTop.ts expo-router
// 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