diff --git a/lib/reducers/calendar-filters-reducer.js b/lib/reducers/calendar-filters-reducer.js
--- a/lib/reducers/calendar-filters-reducer.js
+++ b/lib/reducers/calendar-filters-reducer.js
@@ -23,6 +23,7 @@
   nonExcludeDeletedCalendarFilters,
 } from '../selectors/calendar-filter-selectors.js';
 import { threadInFilterList } from '../shared/thread-utils.js';
+import { updateSpecs } from '../shared/updates/update-specs.js';
 import {
   type CalendarFilter,
   defaultCalendarFilters,
@@ -37,7 +38,6 @@
   incrementalStateSyncActionType,
 } from '../types/socket-types.js';
 import type { RawThreadInfo, ThreadStore } from '../types/thread-types.js';
-import { updateTypes } from '../types/update-types-enum.js';
 import {
   type ClientUpdateInfo,
   processUpdatesActionType,
@@ -138,36 +138,19 @@
   if (!currentlyFilteredIDs) {
     return state;
   }
-  let changeOccurred = false;
-  for (const update of updateInfos) {
-    if (update.type === updateTypes.DELETE_THREAD) {
-      const result = currentlyFilteredIDs.delete(update.threadID);
-      if (result) {
-        changeOccurred = true;
-      }
-    } else if (update.type === updateTypes.JOIN_THREAD) {
-      if (
-        !threadInFilterList(update.threadInfo) ||
-        currentlyFilteredIDs.has(update.threadInfo.id)
-      ) {
-        continue;
-      }
-      currentlyFilteredIDs.add(update.threadInfo.id);
-      changeOccurred = true;
-    } else if (update.type === updateTypes.UPDATE_THREAD) {
-      if (threadInFilterList(update.threadInfo)) {
-        continue;
-      }
-      const result = currentlyFilteredIDs.delete(update.threadInfo.id);
-      if (result) {
-        changeOccurred = true;
-      }
-    }
-  }
-  if (changeOccurred) {
+  const newFilteredThreadIDs = updateInfos.reduce(
+    (reducedFilteredThreadIDs, update) => {
+      const { reduceCalendarThreadFilters } = updateSpecs[update.type];
+      return reduceCalendarThreadFilters
+        ? reduceCalendarThreadFilters(reducedFilteredThreadIDs, update)
+        : reducedFilteredThreadIDs;
+    },
+    currentlyFilteredIDs,
+  );
+  if (currentlyFilteredIDs !== newFilteredThreadIDs) {
     return [
       ...nonThreadCalendarFilters(state),
-      { type: 'threads', threadIDs: [...currentlyFilteredIDs] },
+      { type: 'threads', threadIDs: [...newFilteredThreadIDs] },
     ];
   }
   return state;
diff --git a/lib/shared/updates/delete-thread-spec.js b/lib/shared/updates/delete-thread-spec.js
--- a/lib/shared/updates/delete-thread-spec.js
+++ b/lib/shared/updates/delete-thread-spec.js
@@ -22,4 +22,15 @@
       }
       return null;
     },
+    reduceCalendarThreadFilters(
+      filteredThreadIDs: $ReadOnlySet<string>,
+      update: ThreadDeletionUpdateInfo,
+    ) {
+      if (!filteredThreadIDs.has(update.threadID)) {
+        return filteredThreadIDs;
+      }
+      return new Set(
+        [...filteredThreadIDs].filter(id => id !== update.threadID),
+      );
+    },
   });
diff --git a/lib/shared/updates/join-thread-spec.js b/lib/shared/updates/join-thread-spec.js
--- a/lib/shared/updates/join-thread-spec.js
+++ b/lib/shared/updates/join-thread-spec.js
@@ -6,6 +6,7 @@
 import type { RawEntryInfo } from '../../types/entry-types.js';
 import type { RawThreadInfos } from '../../types/thread-types.js';
 import type { ThreadJoinUpdateInfo } from '../../types/update-types.js';
+import { threadInFilterList } from '../thread-utils.js';
 
 export const joinThreadSpec: UpdateSpec<ThreadJoinUpdateInfo> = Object.freeze({
   generateOpsForThreadUpdates(
@@ -39,4 +40,16 @@
       entryIDs.add(entryID);
     }
   },
+  reduceCalendarThreadFilters(
+    filteredThreadIDs: $ReadOnlySet<string>,
+    update: ThreadJoinUpdateInfo,
+  ) {
+    if (
+      !threadInFilterList(update.threadInfo) ||
+      filteredThreadIDs.has(update.threadInfo.id)
+    ) {
+      return filteredThreadIDs;
+    }
+    return new Set([...filteredThreadIDs, update.threadInfo.id]);
+  },
 });
diff --git a/lib/shared/updates/update-spec.js b/lib/shared/updates/update-spec.js
--- a/lib/shared/updates/update-spec.js
+++ b/lib/shared/updates/update-spec.js
@@ -21,4 +21,8 @@
     update: UpdateInfo,
   ) => ?CurrentUserInfo,
   +reduceUserInfos?: (state: UserInfos, update: UpdateInfo) => UserInfos,
+  +reduceCalendarThreadFilters?: (
+    filteredThreadIDs: $ReadOnlySet<string>,
+    update: UpdateInfo,
+  ) => $ReadOnlySet<string>,
 };
diff --git a/lib/shared/updates/update-thread-spec.js b/lib/shared/updates/update-thread-spec.js
--- a/lib/shared/updates/update-thread-spec.js
+++ b/lib/shared/updates/update-thread-spec.js
@@ -5,6 +5,7 @@
 import type { UpdateSpec } from './update-spec.js';
 import type { RawThreadInfos } from '../../types/thread-types.js';
 import type { ThreadUpdateInfo } from '../../types/update-types.js';
+import { threadInFilterList } from '../thread-utils.js';
 
 export const updateThreadSpec: UpdateSpec<ThreadUpdateInfo> = Object.freeze({
   generateOpsForThreadUpdates(
@@ -24,4 +25,18 @@
       },
     ];
   },
+  reduceCalendarThreadFilters(
+    filteredThreadIDs: $ReadOnlySet<string>,
+    update: ThreadUpdateInfo,
+  ) {
+    if (
+      threadInFilterList(update.threadInfo) ||
+      !filteredThreadIDs.has(update.threadInfo.id)
+    ) {
+      return filteredThreadIDs;
+    }
+    return new Set(
+      [...filteredThreadIDs].filter(id => id !== update.threadInfo.id),
+    );
+  },
 });