A React Native app for the ultimate thinking partner.
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add scroll-to-bottom on mount with reusable hook

Implement automatic scroll-to-bottom when chat loads using a new
useScrollToBottom hook. The hook provides configurable scroll behavior
for initial mount and content changes, improving UX by showing the most
recent messages immediately.

- Add useScrollToBottom hook with scrollOnMount, delay, and animated options
- Integrate onContentSizeChange to detect when to scroll
- Non-animated scroll on mount, animated scroll when sending messages
- Reusable across any list/chat component

+94 -9
+84
src/hooks/useScrollToBottom.ts
··· 1 + import { useRef, useEffect, useCallback } from 'react'; 2 + import { FlatList } from 'react-native'; 3 + 4 + interface UseScrollToBottomOptions { 5 + /** 6 + * Whether to scroll to bottom on initial mount/load 7 + * @default true 8 + */ 9 + scrollOnMount?: boolean; 10 + 11 + /** 12 + * Delay before scrolling (ms) - useful for rendering completion 13 + * @default 100 14 + */ 15 + delay?: number; 16 + 17 + /** 18 + * Whether to animate the scroll 19 + * @default true 20 + */ 21 + animated?: boolean; 22 + } 23 + 24 + /** 25 + * Hook for managing scroll-to-bottom behavior in chat interfaces 26 + * 27 + * @example 28 + * const { scrollViewRef, scrollToBottom, onContentSizeChange } = useScrollToBottom({ 29 + * scrollOnMount: true, 30 + * delay: 100 31 + * }); 32 + * 33 + * <FlatList 34 + * ref={scrollViewRef} 35 + * onContentSizeChange={onContentSizeChange} 36 + * /> 37 + */ 38 + export function useScrollToBottom(options: UseScrollToBottomOptions = {}) { 39 + const { 40 + scrollOnMount = true, 41 + delay = 100, 42 + animated = true, 43 + } = options; 44 + 45 + const scrollViewRef = useRef<FlatList<any>>(null); 46 + const hasMountedRef = useRef(false); 47 + const contentSizeRef = useRef({ width: 0, height: 0 }); 48 + 49 + const scrollToBottom = useCallback((customAnimated?: boolean) => { 50 + setTimeout(() => { 51 + scrollViewRef.current?.scrollToEnd({ 52 + animated: customAnimated !== undefined ? customAnimated : animated 53 + }); 54 + }, delay); 55 + }, [delay, animated]); 56 + 57 + // Handle content size change - scroll to bottom if content grows 58 + const onContentSizeChange = useCallback((width: number, height: number) => { 59 + const hasContentGrown = height > contentSizeRef.current.height; 60 + contentSizeRef.current = { width, height }; 61 + 62 + // Scroll if this is the first render with content, or if content has grown 63 + if (!hasMountedRef.current && height > 0 && scrollOnMount) { 64 + hasMountedRef.current = true; 65 + scrollToBottom(false); // Don't animate initial scroll 66 + } else if (hasContentGrown && hasMountedRef.current) { 67 + // Content grew after mount - could be new message 68 + scrollToBottom(true); 69 + } 70 + }, [scrollOnMount, scrollToBottom]); 71 + 72 + // Scroll on mount if requested 73 + useEffect(() => { 74 + if (scrollOnMount) { 75 + scrollToBottom(false); 76 + } 77 + }, [scrollOnMount, scrollToBottom]); 78 + 79 + return { 80 + scrollViewRef, 81 + scrollToBottom, 82 + onContentSizeChange, 83 + }; 84 + }
+10 -9
src/screens/ChatScreen.tsx
··· 13 13 import { useMessageStream } from '../hooks/useMessageStream'; 14 14 import { useChatStore } from '../stores/chatStore'; 15 15 import { useMessageInteractions } from '../hooks/useMessageInteractions'; 16 + import { useScrollToBottom } from '../hooks/useScrollToBottom'; 16 17 17 18 import MessageBubbleEnhanced from '../components/MessageBubble.enhanced'; 18 19 import MessageInputEnhanced from '../components/MessageInputEnhanced'; ··· 25 26 26 27 export function ChatScreen({ theme, colorScheme, showCompaction = true }: ChatScreenProps) { 27 28 const insets = useSafeAreaInsets(); 28 - const scrollViewRef = useRef<FlatList<any>>(null); 29 29 30 30 // Hooks 31 31 const { messages, isLoadingMessages, loadMoreMessages, hasMoreBefore, isLoadingMore } = useMessages(); ··· 40 40 toggleToolReturn, 41 41 copyToClipboard, 42 42 } = useMessageInteractions(); 43 + 44 + // Scroll management 45 + const { scrollViewRef, scrollToBottom, onContentSizeChange } = useScrollToBottom({ 46 + scrollOnMount: true, 47 + delay: 150, 48 + animated: false, 49 + }); 43 50 44 51 // Filter and sort messages for display 45 52 const displayMessages = React.useMemo(() => { ··· 80 87 const spacerHeightAnim = useRef(new Animated.Value(0)).current; 81 88 const [containerHeight, setContainerHeight] = React.useState(0); 82 89 83 - // Scroll to bottom 84 - const scrollToBottom = () => { 85 - setTimeout(() => { 86 - scrollViewRef.current?.scrollToEnd({ animated: true }); 87 - }, 100); 88 - }; 89 - 90 90 // Handle send message 91 91 const handleSend = async (text: string) => { 92 92 await sendMessage(text, selectedImages); 93 - scrollToBottom(); 93 + scrollToBottom(true); // Animate scroll when sending 94 94 }; 95 95 96 96 // Render message item ··· 131 131 styles.messagesList, 132 132 { paddingBottom: insets.bottom + 80 }, 133 133 ]} 134 + onContentSizeChange={onContentSizeChange} 134 135 onEndReached={loadMoreMessages} 135 136 onEndReachedThreshold={0.5} 136 137 initialNumToRender={100}