@@ -479,6 +479,13 @@ export const TopicMessageView: FC<TopicMessageViewProps> = (props) => {
479479 const prevPageIndexRef = useRef < number > ( pageIndex ) ;
480480 const [ forceRefresh , setForceRefresh ] = useState ( 0 ) ;
481481
482+ // Refs for tracking loadMore state to prevent stale closures and memory leaks
483+ const currentMessageSearchRef = useRef < ReturnType < typeof createMessageSearch > | null > ( null ) ;
484+ const loadMoreAbortRef = useRef < AbortController | null > ( null ) ;
485+ const [ loadMoreFailures , setLoadMoreFailures ] = useState ( 0 ) ;
486+ const isMountedRef = useRef ( true ) ;
487+ const MAX_LOAD_MORE_RETRIES = 3 ;
488+
482489 // Filter messages based on quick search
483490 const filteredMessages = quickSearch
484491 ? messages . filter ( ( m ) => {
@@ -497,16 +504,25 @@ export const TopicMessageView: FC<TopicMessageViewProps> = (props) => {
497504 [ topicSettings ?. previewTags ]
498505 ) ;
499506
507+ // Keep currentMessageSearchRef in sync with messageSearch state
508+ useEffect ( ( ) => {
509+ currentMessageSearchRef . current = messageSearch ;
510+ } , [ messageSearch ] ) ;
511+
500512 // Cleanup effect (replaces componentWillUnmount)
501- useEffect (
502- ( ) => ( ) => {
513+ useEffect ( ( ) => {
514+ isMountedRef . current = true ;
515+ return ( ) => {
516+ isMountedRef . current = false ;
503517 if ( abortControllerRef . current ) {
504518 abortControllerRef . current . abort ( ) ;
505519 }
520+ if ( loadMoreAbortRef . current ) {
521+ loadMoreAbortRef . current . abort ( ) ;
522+ }
506523 appGlobal . searchMessagesFunc = undefined ;
507- } ,
508- [ ]
509- ) ;
524+ } ;
525+ } , [ ] ) ;
510526
511527 // Clear sorting when entering unlimited pagination mode
512528 useEffect ( ( ) => {
@@ -728,7 +744,8 @@ export const TopicMessageView: FC<TopicMessageViewProps> = (props) => {
728744 ! messageSearch ||
729745 ! messageSearch . nextPageToken ||
730746 isLoadingMore ||
731- searchPhase
747+ searchPhase ||
748+ loadMoreFailures >= MAX_LOAD_MORE_RETRIES
732749 ) {
733750 return ;
734751 }
@@ -737,23 +754,44 @@ export const TopicMessageView: FC<TopicMessageViewProps> = (props) => {
737754 const isOnLastPage = pageIndex >= totalLoadedPages - 1 ;
738755
739756 if ( isOnLastPage && messageSearch . nextPageToken ) {
757+ // Create abort controller for this loadMore operation
758+ const abortController = new AbortController ( ) ;
759+ loadMoreAbortRef . current = abortController ;
760+
761+ // Capture the current messageSearch reference to detect stale responses
762+ const capturedMessageSearch = messageSearch ;
763+
740764 setIsLoadingMore ( true ) ;
741- messageSearch
765+ capturedMessageSearch
742766 . loadMore ( )
743767 . then ( ( ) => {
744- setMessages ( [ ...messageSearch . messages ] ) ;
768+ // Only update state if component is still mounted and this is still the current search
769+ if ( isMountedRef . current && currentMessageSearchRef . current === capturedMessageSearch ) {
770+ setMessages ( [ ...capturedMessageSearch . messages ] ) ;
771+ // Reset failure count on success
772+ setLoadMoreFailures ( 0 ) ;
773+ }
745774 } )
746775 . catch ( ( err ) => {
747- toast ( {
748- title : 'Failed to load more messages' ,
749- description : ( err as Error ) . message ,
750- status : 'error' ,
751- duration : 5000 ,
752- isClosable : true ,
753- } ) ;
776+ // Only show error if component is still mounted and not aborted
777+ if ( isMountedRef . current && ! abortController . signal . aborted ) {
778+ setLoadMoreFailures ( ( prev ) => prev + 1 ) ;
779+ toast ( {
780+ title : 'Failed to load more messages' ,
781+ description : ( err as Error ) . message ,
782+ status : 'error' ,
783+ duration : 5000 ,
784+ isClosable : true ,
785+ } ) ;
786+ }
754787 } )
755788 . finally ( ( ) => {
756- setIsLoadingMore ( false ) ;
789+ if ( isMountedRef . current ) {
790+ setIsLoadingMore ( false ) ;
791+ }
792+ if ( loadMoreAbortRef . current === abortController ) {
793+ loadMoreAbortRef . current = null ;
794+ }
757795 } ) ;
758796 }
759797 } , [
@@ -766,22 +804,28 @@ export const TopicMessageView: FC<TopicMessageViewProps> = (props) => {
766804 messages . length ,
767805 pageSize ,
768806 toast ,
807+ loadMoreFailures ,
769808 ] ) ;
770809
771810 // Reset pagination when navigating back to page 1 in unlimited mode
772811 // This prevents keeping many pages in memory and triggering excessive requests
773812 useEffect ( ( ) => {
774813 // Check if we're in unlimited mode and user navigated back to page 1 from a higher page
775- if ( maxResults === - 1 && pageIndex === 0 && prevPageIndexRef . current > 1 && messageSearch ) {
814+ // Use ref to check messageSearch existence to avoid circular dependency
815+ if ( maxResults === - 1 && pageIndex === 0 && prevPageIndexRef . current > 1 && currentMessageSearchRef . current ) {
776816 // Clear the message search and state
777817 setMessages ( [ ] ) ;
778818 setMessageSearch ( null ) ;
819+ // Reset failure count when resetting pagination
820+ setLoadMoreFailures ( 0 ) ;
779821 // Clear the search run ref and trigger a forced refresh
780822 currentSearchRunRef . current = null ;
781823 setForceRefresh ( ( prev ) => prev + 1 ) ;
782824 }
783825 prevPageIndexRef . current = pageIndex ;
784- } , [ pageIndex , maxResults , messageSearch ] ) ;
826+ // Note: messageSearch intentionally excluded to avoid circular dependency
827+ // We use currentMessageSearchRef.current instead which is always in sync
828+ } , [ pageIndex , maxResults ] ) ;
785829
786830 // Message Table rendering variables and functions
787831 const paginationParams = {
0 commit comments