diff --git a/lib/hooks/ens-cache.js b/lib/hooks/ens-cache.js
--- a/lib/hooks/ens-cache.js
+++ b/lib/hooks/ens-cache.js
@@ -6,11 +6,19 @@
 import { ENSCacheContext } from '../components/ens-cache-provider.react.js';
 import { getETHAddressForUserInfo } from '../shared/account-utils.js';
 import { stringForUser } from '../shared/user-utils.js';
+import { getENSNames } from '../utils/ens-helpers.js';
 
 type BaseUserInfo = { +username?: ?string, ... };
-function useENSNames<T: ?BaseUserInfo>(users: $ReadOnlyArray<T>): T[] {
+export type UseENSNamesOptions = {
+  +allAtOnce?: ?boolean,
+};
+function useENSNames<T: ?BaseUserInfo>(
+  users: $ReadOnlyArray<T>,
+  options?: ?UseENSNamesOptions,
+): T[] {
   const cacheContext = React.useContext(ENSCacheContext);
   const { ensCache } = cacheContext;
+  const allAtOnce = options?.allAtOnce ?? false;
 
   const cachedInfo = React.useMemo(
     () =>
@@ -43,7 +51,8 @@
     if (!ensCache) {
       return;
     }
-    const needFetch = cachedInfo
+
+    const needFetchUsers: $ReadOnlyArray<{ +username: string }> = cachedInfo
       .map(user => {
         if (!user) {
           return null;
@@ -52,20 +61,39 @@
         if (cachedResult || !ethAddress || fetchedAddresses.has(ethAddress)) {
           return null;
         }
-        return ethAddress;
+        return { username: ethAddress };
       })
       .filter(Boolean);
-    if (needFetch.length === 0) {
+    if (needFetchUsers.length === 0) {
       return;
     }
+
+    const needFetchAddresses = needFetchUsers.map(({ username }) => username);
     setFetchedAddresses(oldFetchedAddresses => {
       const newFetchedAddresses = new Set(oldFetchedAddresses);
-      for (const ethAddress of needFetch) {
+      for (const ethAddress of needFetchAddresses) {
         newFetchedAddresses.add(ethAddress);
       }
       return newFetchedAddresses;
     });
-    for (const ethAddress of needFetch) {
+
+    if (allAtOnce) {
+      (async () => {
+        const withENSNames = await getENSNames(ensCache, needFetchUsers);
+        setENSNames(oldENSNames => {
+          const newENSNames = new Map(oldENSNames);
+          for (let i = 0; i < withENSNames.length; i++) {
+            const ethAddress = needFetchAddresses[i];
+            const result = withENSNames[i].username;
+            newENSNames.set(ethAddress, result);
+          }
+          return newENSNames;
+        });
+      })();
+      return;
+    }
+
+    for (const ethAddress of needFetchAddresses) {
       (async () => {
         const result = await ensCache.getNameForAddress(ethAddress);
         if (!result) {
@@ -78,7 +106,7 @@
         });
       })();
     }
-  }, [cachedInfo, fetchedAddresses, ensCache]);
+  }, [cachedInfo, fetchedAddresses, ensCache, allAtOnce]);
 
   return React.useMemo(
     () =>
diff --git a/lib/selectors/nav-selectors.js b/lib/selectors/nav-selectors.js
--- a/lib/selectors/nav-selectors.js
+++ b/lib/selectors/nav-selectors.js
@@ -3,7 +3,7 @@
 import * as React from 'react';
 import { createSelector } from 'reselect';
 
-import { ENSCacheContext } from '../components/ens-cache-provider.react.js';
+import { useENSNames } from '../hooks/ens-cache.js';
 import SearchIndex from '../shared/search-index.js';
 import { memberHasAdminPowers } from '../shared/thread-utils.js';
 import type { Platform } from '../types/device-types.js';
@@ -16,7 +16,6 @@
 import type { BaseAppState } from '../types/redux-types.js';
 import type { RawThreadInfo, ThreadInfo } from '../types/thread-types.js';
 import { getConfig } from '../utils/config.js';
-import { getENSNames } from '../utils/ens-helpers.js';
 import { values } from '../utils/objects.js';
 import { useSelector } from '../utils/redux-utils.js';
 
@@ -72,6 +71,12 @@
   },
 );
 
+// Without allAtOnce, useThreadSearchIndex is very expensive. useENSNames would
+// trigger its recalculation for each ENS name as it streams in, but we would
+// prefer to trigger its recaculation just once for every update of the
+// underlying Redux data.
+const useENSNamesOptions = { allAtOnce: true };
+
 function useThreadSearchIndex(
   threadInfos: $ReadOnlyArray<RawThreadInfo | ThreadInfo>,
 ): SearchIndex {
@@ -101,34 +106,18 @@
     return [...allMembersOfAllThreads.values()];
   }, [threadInfos, userInfos, viewerID]);
 
-  const cacheContext = React.useContext(ENSCacheContext);
-  const { ensCache } = cacheContext;
-
-  // We avoid using useENSNames here because the SearchIndex memo below is very
-  // expensive. useENSNames would trigger its recalculation for each ENS name as
-  // it streams in, but we would prefer to trigger its recaculation just once
-  // for every update of the underlying Redux data.
-  const [nonViewerMembersWithENSNames, setNonViewerMembersWithENSNames] =
-    React.useState();
-  React.useEffect(() => {
-    if (!ensCache) {
-      return;
-    }
-    (async () => {
-      const withENSNames = await getENSNames(ensCache, nonViewerMembers);
-      setNonViewerMembersWithENSNames(withENSNames);
-    })();
-  }, [ensCache, nonViewerMembers]);
+  const nonViewerMembersWithENSNames = useENSNames(
+    nonViewerMembers,
+    useENSNamesOptions,
+  );
 
-  const resolvedNonViewerMembers =
-    nonViewerMembersWithENSNames ?? nonViewerMembers;
   const memberMap = React.useMemo(() => {
     const result = new Map();
-    for (const userInfo of resolvedNonViewerMembers) {
+    for (const userInfo of nonViewerMembersWithENSNames) {
       result.set(userInfo.id, userInfo);
     }
     return result;
-  }, [resolvedNonViewerMembers]);
+  }, [nonViewerMembersWithENSNames]);
 
   return React.useMemo(() => {
     const searchIndex = new SearchIndex();