diff --git a/lib/hooks/search-threads.js b/lib/hooks/search-threads.js
--- a/lib/hooks/search-threads.js
+++ b/lib/hooks/search-threads.js
@@ -8,6 +8,11 @@
   useFilteredChatListData,
 } from '../selectors/chat-selectors.js';
 import { useThreadSearchIndex } from '../selectors/nav-selectors.js';
+import {
+  type SidebarThreadItem,
+  getAllInitialSidebarItems,
+  getAllFinalSidebarItems,
+} from '../shared/sidebar-item-utils.js';
 import { threadIsChannel } from '../shared/thread-utils.js';
 import type { SetState } from '../types/hook-types.js';
 import type {
@@ -29,7 +34,11 @@
   +clearQuery: (event: SyntheticEvent<HTMLAnchorElement>) => void,
 };
 
-function useSearchThreads<U: SidebarInfo | ChatThreadItem>(
+type ChildThreadInfos = {
+  +threadInfo: RawThreadInfo | ThreadInfo,
+  ...
+};
+function useSearchThreads<U: ChildThreadInfos>(
   threadInfo: ThreadInfo,
   childThreadInfos: $ReadOnlyArray<U>,
 ): SearchThreadsResult<U> {
@@ -93,10 +102,72 @@
 
 function useSearchSidebars(
   threadInfo: ThreadInfo,
-): SearchThreadsResult<SidebarInfo> {
+): SearchThreadsResult<SidebarThreadItem> {
   const sidebarsByParentID = useSidebarInfos();
   const childThreadInfos = sidebarsByParentID[threadInfo.id] ?? emptyArray;
-  return useSearchThreads(threadInfo, childThreadInfos);
+  const initialSidebarItems = React.useMemo(
+    () => getAllInitialSidebarItems(childThreadInfos),
+    [childThreadInfos],
+  );
+  const [sidebarItems, setSidebarItems] =
+    React.useState<$ReadOnlyArray<SidebarThreadItem>>(initialSidebarItems);
+
+  const prevChildThreadInfosRef = React.useRef(childThreadInfos);
+  React.useEffect(() => {
+    if (childThreadInfos === prevChildThreadInfosRef.current) {
+      return;
+    }
+    prevChildThreadInfosRef.current = childThreadInfos;
+
+    setSidebarItems(initialSidebarItems);
+
+    void (async () => {
+      const finalSidebarItems = await getAllFinalSidebarItems(childThreadInfos);
+      if (childThreadInfos !== prevChildThreadInfosRef.current) {
+        // If these aren't equal, it indicates that the effect has fired again.
+        // We should discard this result as it is now outdated.
+        return;
+      }
+      // The callback below is basically setSidebarItems(finalSidebarItems), but
+      // it has extra logic to preserve objects if they are unchanged.
+      setSidebarItems(prevSidebarItems => {
+        if (prevSidebarItems.length !== finalSidebarItems.length) {
+          console.log(
+            'unexpected: prevSidebarItems.length !== finalSidebarItems.length',
+          );
+          return finalSidebarItems;
+        }
+        let somethingChanged = false;
+        const result = [];
+        for (let i = 0; i < prevSidebarItems.length; i++) {
+          const prevSidebarItem = prevSidebarItems[i];
+          const newSidebarItem = finalSidebarItems[i];
+          if (prevSidebarItem.threadInfo.id !== newSidebarItem.threadInfo.id) {
+            console.log(
+              'unexpected: prevSidebarItem.threadInfo.id !== ' +
+                'newSidebarItem.threadInfo.id',
+            );
+            return finalSidebarItems;
+          }
+          if (
+            prevSidebarItem.lastUpdatedTime !== newSidebarItem.lastUpdatedTime
+          ) {
+            somethingChanged = true;
+            result[i] = newSidebarItem;
+          } else {
+            result[i] = prevSidebarItem;
+          }
+        }
+        if (somethingChanged) {
+          return result;
+        } else {
+          return prevSidebarItems;
+        }
+      });
+    })();
+  }, [childThreadInfos, initialSidebarItems]);
+
+  return useSearchThreads(threadInfo, sidebarItems);
 }
 
 function useSearchSubchannels(
diff --git a/lib/hooks/sidebar-hooks.js b/lib/hooks/sidebar-hooks.js
--- a/lib/hooks/sidebar-hooks.js
+++ b/lib/hooks/sidebar-hooks.js
@@ -28,7 +28,7 @@
         ) {
           continue;
         }
-        const { lastUpdatedAtLeastTime: lastUpdatedTime } = getLastUpdatedTimes(
+        const { lastUpdatedTime, lastUpdatedAtLeastTime } = getLastUpdatedTimes(
           childThreadInfo,
           messageStore,
           messageStore.messages,
@@ -40,6 +40,7 @@
         sidebarInfos.push({
           threadInfo: childThreadInfo,
           lastUpdatedTime,
+          lastUpdatedAtLeastTime,
           mostRecentNonLocalMessage,
         });
       }
diff --git a/lib/selectors/chat-selectors.js b/lib/selectors/chat-selectors.js
--- a/lib/selectors/chat-selectors.js
+++ b/lib/selectors/chat-selectors.js
@@ -25,6 +25,7 @@
 import { messageSpecs } from '../shared/messages/message-specs.js';
 import {
   getSidebarItems,
+  getAllInitialSidebarItems,
   type SidebarItem,
 } from '../shared/sidebar-item-utils.js';
 import { threadInChatList, threadIsPending } from '../shared/thread-utils.js';
@@ -92,30 +93,28 @@
         messageStore,
       );
 
-      const { lastUpdatedAtLeastTime: lastUpdatedTime } = getLastUpdatedTimes(
+      const { lastUpdatedAtLeastTime } = getLastUpdatedTimes(
         threadInfo,
         messageStore,
         messageInfos,
       );
 
       const sidebars = sidebarInfos[threadInfo.id] ?? [];
-      const allSidebarItems = sidebars.map(sidebarInfo => ({
-        type: 'sidebar',
-        ...sidebarInfo,
-      }));
-      const lastUpdatedTimeIncludingSidebars =
-        allSidebarItems.length > 0
-          ? Math.max(lastUpdatedTime, allSidebarItems[0].lastUpdatedTime)
-          : lastUpdatedTime;
+      const lastUpdatedAtLeastTimeIncludingSidebars =
+        sidebars.length > 0
+          ? Math.max(lastUpdatedAtLeastTime, sidebars[0].lastUpdatedAtLeastTime)
+          : lastUpdatedAtLeastTime;
 
-      const sidebarItems = getSidebarItems(allSidebarItems);
+      const allInitialSidebarItems = getAllInitialSidebarItems(sidebars);
+      const sidebarItems = getSidebarItems(allInitialSidebarItems);
 
       return {
         type: 'chatThreadItem',
         threadInfo,
         mostRecentNonLocalMessage,
-        lastUpdatedTime,
-        lastUpdatedTimeIncludingSidebars,
+        lastUpdatedTime: lastUpdatedAtLeastTime,
+        lastUpdatedTimeIncludingSidebars:
+          lastUpdatedAtLeastTimeIncludingSidebars,
         sidebars: sidebarItems,
       };
     },
diff --git a/lib/shared/sidebar-item-utils.js b/lib/shared/sidebar-item-utils.js
--- a/lib/shared/sidebar-item-utils.js
+++ b/lib/shared/sidebar-item-utils.js
@@ -1,10 +1,14 @@
 // @flow
 
 import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js';
-import { maxReadSidebars, maxUnreadSidebars } from '../types/thread-types.js';
+import {
+  maxReadSidebars,
+  maxUnreadSidebars,
+  type SidebarInfo,
+} from '../types/thread-types.js';
 import { threeDays } from '../utils/date-utils.js';
 
-type SidebarThreadItem = {
+export type SidebarThreadItem = {
   +type: 'sidebar',
   +threadInfo: ThreadInfo,
   +mostRecentNonLocalMessage: ?string,
@@ -65,4 +69,32 @@
   return sidebarItems;
 }
 
-export { getSidebarItems };
+function getAllInitialSidebarItems(
+  sidebarInfos: $ReadOnlyArray<SidebarInfo>,
+): SidebarThreadItem[] {
+  return sidebarInfos.map(sidebarItem => {
+    const { lastUpdatedTime, lastUpdatedAtLeastTime, ...rest } = sidebarItem;
+    return {
+      ...rest,
+      type: 'sidebar',
+      lastUpdatedTime: lastUpdatedAtLeastTime,
+    };
+  });
+}
+
+async function getAllFinalSidebarItems(
+  sidebarInfos: $ReadOnlyArray<SidebarInfo>,
+): Promise<$ReadOnlyArray<SidebarThreadItem>> {
+  const allSidebarItemPromises = sidebarInfos.map(async sidebarItem => {
+    const { lastUpdatedTime, lastUpdatedAtLeastTime, ...rest } = sidebarItem;
+    const finalLastUpdatedTime = await lastUpdatedTime;
+    return {
+      ...rest,
+      type: 'sidebar',
+      lastUpdatedTime: finalLastUpdatedTime,
+    };
+  });
+  return await Promise.all(allSidebarItemPromises);
+}
+
+export { getSidebarItems, getAllInitialSidebarItems, getAllFinalSidebarItems };
diff --git a/lib/types/thread-types.js b/lib/types/thread-types.js
--- a/lib/types/thread-types.js
+++ b/lib/types/thread-types.js
@@ -443,11 +443,11 @@
   +lastUpdatedTime: Promise<number>,
 };
 
-export type SidebarInfo = {
+export type SidebarInfo = $ReadOnly<{
+  ...LastUpdatedTimes,
   +threadInfo: ThreadInfo,
-  +lastUpdatedTime: number,
   +mostRecentNonLocalMessage: ?string,
-};
+}>;
 
 export type ToggleMessagePinRequest = {
   +messageID: string,
diff --git a/native/chat/chat-thread-list-item.react.js b/native/chat/chat-thread-list-item.react.js
--- a/native/chat/chat-thread-list-item.react.js
+++ b/native/chat/chat-thread-list-item.react.js
@@ -50,10 +50,9 @@
     () =>
       data.sidebars.map((sidebarItem, index) => {
         if (sidebarItem.type === 'sidebar') {
-          const { type, ...sidebarInfo } = sidebarItem;
           return (
             <ChatThreadListSidebar
-              sidebarInfo={sidebarInfo}
+              sidebarItem={sidebarItem}
               onPressItem={onPressItem}
               onSwipeableWillOpen={onSwipeableWillOpen}
               currentlyOpenedSwipeableId={currentlyOpenedSwipeableId}
diff --git a/native/chat/chat-thread-list-sidebar.react.js b/native/chat/chat-thread-list-sidebar.react.js
--- a/native/chat/chat-thread-list-sidebar.react.js
+++ b/native/chat/chat-thread-list-sidebar.react.js
@@ -3,8 +3,8 @@
 import * as React from 'react';
 import { View } from 'react-native';
 
+import type { SidebarThreadItem } from 'lib/shared/sidebar-item-utils.js';
 import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js';
-import type { SidebarInfo } from 'lib/types/thread-types.js';
 
 import { sidebarHeight, SidebarItem } from './sidebar-item.react.js';
 import SwipeableThread from './swipeable-thread.react.js';
@@ -15,7 +15,7 @@
 import Arrow from '../vectors/arrow.react.js';
 
 type Props = {
-  +sidebarInfo: SidebarInfo,
+  +sidebarItem: SidebarThreadItem,
   +onPressItem: (threadInfo: ThreadInfo) => void,
   +onSwipeableWillOpen: (threadInfo: ThreadInfo) => void,
   +currentlyOpenedSwipeableId: string,
@@ -26,14 +26,14 @@
   const styles = useStyles(unboundStyles);
 
   const {
-    sidebarInfo,
+    sidebarItem,
     onSwipeableWillOpen,
     currentlyOpenedSwipeableId,
     onPressItem,
     extendArrow = false,
   } = props;
 
-  const { threadInfo } = sidebarInfo;
+  const { threadInfo } = sidebarItem;
 
   const onPress = React.useCallback(
     () => onPressItem(threadInfo),
@@ -58,40 +58,37 @@
   const unreadIndicator = React.useMemo(
     () => (
       <View style={styles.unreadIndicatorContainer}>
-        <UnreadDot unread={sidebarInfo.threadInfo.currentUser.unread} />
+        <UnreadDot unread={threadInfo.currentUser.unread} />
       </View>
     ),
-    [
-      sidebarInfo.threadInfo.currentUser.unread,
-      styles.unreadIndicatorContainer,
-    ],
+    [threadInfo.currentUser.unread, styles.unreadIndicatorContainer],
   );
 
-  const sidebarItem = React.useMemo(
-    () => <SidebarItem sidebarInfo={sidebarInfo} />,
-    [sidebarInfo],
+  const sidebarItemElement = React.useMemo(
+    () => <SidebarItem sidebarItem={sidebarItem} />,
+    [sidebarItem],
   );
 
   const swipeableThread = React.useMemo(
     () => (
       <View style={styles.swipeableThreadContainer}>
         <SwipeableThread
-          threadInfo={sidebarInfo.threadInfo}
-          mostRecentNonLocalMessage={sidebarInfo.mostRecentNonLocalMessage}
+          threadInfo={threadInfo}
+          mostRecentNonLocalMessage={sidebarItem.mostRecentNonLocalMessage}
           onSwipeableWillOpen={onSwipeableWillOpen}
           currentlyOpenedSwipeableId={currentlyOpenedSwipeableId}
           iconSize={16}
         >
-          {sidebarItem}
+          {sidebarItemElement}
         </SwipeableThread>
       </View>
     ),
     [
       currentlyOpenedSwipeableId,
       onSwipeableWillOpen,
-      sidebarInfo.mostRecentNonLocalMessage,
-      sidebarInfo.threadInfo,
-      sidebarItem,
+      sidebarItem.mostRecentNonLocalMessage,
+      threadInfo,
+      sidebarItemElement,
       styles.swipeableThreadContainer,
     ],
   );
diff --git a/native/chat/sidebar-item.react.js b/native/chat/sidebar-item.react.js
--- a/native/chat/sidebar-item.react.js
+++ b/native/chat/sidebar-item.react.js
@@ -3,7 +3,7 @@
 import * as React from 'react';
 import { Text, View } from 'react-native';
 
-import type { SidebarInfo } from 'lib/types/thread-types.js';
+import type { SidebarThreadItem } from 'lib/shared/sidebar-item-utils.js';
 import { shortAbsoluteDate } from 'lib/utils/date-utils.js';
 import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js';
 
@@ -11,17 +11,17 @@
 import { useStyles } from '../themes/colors.js';
 
 type Props = {
-  +sidebarInfo: SidebarInfo,
+  +sidebarItem: SidebarThreadItem,
 };
 function SidebarItem(props: Props): React.Node {
-  const { lastUpdatedTime } = props.sidebarInfo;
+  const { lastUpdatedTime } = props.sidebarItem;
 
   const lastActivity = React.useMemo(
     () => shortAbsoluteDate(lastUpdatedTime),
     [lastUpdatedTime],
   );
 
-  const { threadInfo } = props.sidebarInfo;
+  const { threadInfo } = props.sidebarItem;
   const { uiName } = useResolvedThreadInfo(threadInfo);
   const styles = useStyles(unboundStyles);
   const unreadStyle = threadInfo.currentUser.unread ? styles.unread : null;
diff --git a/native/chat/sidebar-list-modal.react.js b/native/chat/sidebar-list-modal.react.js
--- a/native/chat/sidebar-list-modal.react.js
+++ b/native/chat/sidebar-list-modal.react.js
@@ -4,8 +4,8 @@
 import { View } from 'react-native';
 
 import { useSearchSidebars } from 'lib/hooks/search-threads.js';
+import type { SidebarThreadItem } from 'lib/shared/sidebar-item-utils.js';
 import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js';
-import type { SidebarInfo } from 'lib/types/thread-types.js';
 
 import { SidebarItem } from './sidebar-item.react.js';
 import ThreadListModal from './thread-list-modal.react.js';
@@ -33,7 +33,7 @@
   const createRenderItem = React.useCallback(
     (onPressItem: (threadInfo: ThreadInfo) => void) =>
       // eslint-disable-next-line react/display-name
-      (row: { +item: SidebarInfo, +index: number, ... }) => {
+      (row: { +item: SidebarThreadItem, +index: number, ... }) => {
         let extendArrow: boolean = false;
         if (row.index < numOfSidebarsWithExtendedArrow) {
           extendArrow = true;
@@ -64,7 +64,7 @@
 }
 
 function Item(props: {
-  item: SidebarInfo,
+  item: SidebarThreadItem,
   onPressItem: (threadInfo: ThreadInfo) => void,
   extendArrow: boolean,
 }): React.Node {
@@ -106,7 +106,7 @@
         {arrow}
         <View style={styles.spacer} />
         <View style={styles.sidebarItemContainer}>
-          <SidebarItem sidebarInfo={item} />
+          <SidebarItem sidebarItem={item} />
         </View>
       </View>
     </Button>
diff --git a/native/chat/thread-list-modal.react.js b/native/chat/thread-list-modal.react.js
--- a/native/chat/thread-list-modal.react.js
+++ b/native/chat/thread-list-modal.react.js
@@ -11,10 +11,11 @@
 } from 'react-native';
 
 import type { ThreadSearchState } from 'lib/hooks/search-threads.js';
-import type { ChatThreadItem } from 'lib/selectors/chat-selectors.js';
 import type { SetState } from 'lib/types/hook-types.js';
-import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js';
-import type { SidebarInfo } from 'lib/types/thread-types.js';
+import type {
+  ThreadInfo,
+  RawThreadInfo,
+} from 'lib/types/minimally-encoded-thread-permissions-types.js';
 
 import { useNavigateToThread } from './message-list-types.js';
 import Modal from '../components/modal.react.js';
@@ -24,13 +25,14 @@
 import { useIndicatorStyle, useStyles } from '../themes/colors.js';
 import { waitForModalInputFocus } from '../utils/timers.js';
 
-function keyExtractor(sidebarInfo: SidebarInfo | ChatThreadItem) {
+type ChatItem = {
+  +threadInfo: RawThreadInfo | ThreadInfo,
+  ...
+};
+function keyExtractor(sidebarInfo: ChatItem) {
   return sidebarInfo.threadInfo.id;
 }
-function getItemLayout(
-  data: ?$ReadOnlyArray<SidebarInfo | ChatThreadItem>,
-  index: number,
-) {
+function getItemLayout(data: ?$ReadOnlyArray<ChatItem>, index: number) {
   return { length: 24, offset: 24 * index, index };
 }
 
@@ -46,9 +48,7 @@
   +searchPlaceholder?: string,
   +modalTitle: string,
 };
-function ThreadListModal<U: SidebarInfo | ChatThreadItem>(
-  props: Props<U>,
-): React.Node {
+function ThreadListModal<U: ChatItem>(props: Props<U>): React.Node {
   const {
     threadInfo: parentThreadInfo,
     searchState,
diff --git a/web/chat/chat-thread-list-item.react.js b/web/chat/chat-thread-list-item.react.js
--- a/web/chat/chat-thread-list-item.react.js
+++ b/web/chat/chat-thread-list-item.react.js
@@ -90,12 +90,11 @@
 
   const sidebars = item.sidebars.map((sidebarItem, index) => {
     if (sidebarItem.type === 'sidebar') {
-      const { type, ...sidebarInfo } = sidebarItem;
       return (
         <ChatThreadListSidebar
-          sidebarInfo={sidebarInfo}
+          sidebarItem={sidebarItem}
           isSubsequentItem={index > 0}
-          key={sidebarInfo.threadInfo.id}
+          key={sidebarItem.threadInfo.id}
         />
       );
     } else if (sidebarItem.type === 'seeMore') {
diff --git a/web/chat/chat-thread-list-sidebar.react.js b/web/chat/chat-thread-list-sidebar.react.js
--- a/web/chat/chat-thread-list-sidebar.react.js
+++ b/web/chat/chat-thread-list-sidebar.react.js
@@ -3,7 +3,7 @@
 import classNames from 'classnames';
 import * as React from 'react';
 
-import type { SidebarInfo } from 'lib/types/thread-types.js';
+import type { SidebarThreadItem } from 'lib/shared/sidebar-item-utils.js';
 
 import ChatThreadListItemMenu from './chat-thread-list-item-menu.react.js';
 import css from './chat-thread-list.css';
@@ -11,12 +11,12 @@
 import { useThreadIsActive } from '../selectors/thread-selectors.js';
 
 type Props = {
-  +sidebarInfo: SidebarInfo,
+  +sidebarItem: SidebarThreadItem,
   +isSubsequentItem: boolean,
 };
 function ChatThreadListSidebar(props: Props): React.Node {
-  const { sidebarInfo, isSubsequentItem } = props;
-  const { threadInfo, mostRecentNonLocalMessage } = sidebarInfo;
+  const { sidebarItem, isSubsequentItem } = props;
+  const { threadInfo, mostRecentNonLocalMessage } = sidebarItem;
   const {
     currentUser: { unread },
     id: threadID,
@@ -35,7 +35,7 @@
       })}
     >
       <div className={css.dotContainer}>{unreadDot}</div>
-      <SidebarItem sidebarInfo={sidebarInfo} extendArrow={isSubsequentItem} />
+      <SidebarItem sidebarItem={sidebarItem} extendArrow={isSubsequentItem} />
       <ChatThreadListItemMenu
         threadInfo={threadInfo}
         mostRecentNonLocalMessage={mostRecentNonLocalMessage}
diff --git a/web/chat/sidebar-item.react.js b/web/chat/sidebar-item.react.js
--- a/web/chat/sidebar-item.react.js
+++ b/web/chat/sidebar-item.react.js
@@ -3,19 +3,19 @@
 import classNames from 'classnames';
 import * as React from 'react';
 
-import type { SidebarInfo } from 'lib/types/thread-types.js';
+import type { SidebarThreadItem } from 'lib/shared/sidebar-item-utils.js';
 import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js';
 
 import css from './chat-thread-list.css';
 import { useOnClickThread } from '../selectors/thread-selectors.js';
 
 type Props = {
-  +sidebarInfo: SidebarInfo,
+  +sidebarItem: SidebarThreadItem,
   +extendArrow?: boolean,
 };
 function SidebarItem(props: Props): React.Node {
   const {
-    sidebarInfo: { threadInfo },
+    sidebarItem: { threadInfo },
     extendArrow = false,
   } = props;
   const {