diff --git a/lib/reducers/thread-reducer.js b/lib/reducers/thread-reducer.js
--- a/lib/reducers/thread-reducer.js
+++ b/lib/reducers/thread-reducer.js
@@ -31,6 +31,7 @@
   type ThreadStoreOperation,
   threadStoreOpsHandlers,
 } from '../ops/thread-store-ops.js';
+import { updateSpecs } from '../shared/updates/update-specs.js';
 import type { BaseAction } from '../types/redux-types.js';
 import {
   type ClientThreadInconsistencyReportCreationRequest,
@@ -45,7 +46,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,
@@ -66,72 +66,15 @@
     ...
   },
 ): $ReadOnlyArray<ThreadStoreOperation> {
-  const threadOperations: ThreadStoreOperation[] = [];
-  for (const update of payload.updatesResult.newUpdates) {
-    if (
-      (update.type === updateTypes.UPDATE_THREAD ||
-        update.type === updateTypes.JOIN_THREAD) &&
-      !_isEqual(threadInfos[update.threadInfo.id])(update.threadInfo)
-    ) {
-      threadOperations.push({
-        type: 'replace',
-        payload: {
-          id: update.threadInfo.id,
-          threadInfo: update.threadInfo,
-        },
-      });
-    } else if (
-      update.type === updateTypes.UPDATE_THREAD_READ_STATUS &&
-      threadInfos[update.threadID] &&
-      threadInfos[update.threadID].currentUser.unread !== update.unread
-    ) {
-      const updatedThread = {
-        ...threadInfos[update.threadID],
-        currentUser: {
-          ...threadInfos[update.threadID].currentUser,
-          unread: update.unread,
-        },
-      };
-      threadOperations.push({
-        type: 'replace',
-        payload: {
-          id: update.threadID,
-          threadInfo: updatedThread,
-        },
-      });
-    } else if (
-      update.type === updateTypes.DELETE_THREAD &&
-      threadInfos[update.threadID]
-    ) {
-      threadOperations.push({
-        type: 'remove',
-        payload: {
-          ids: [update.threadID],
-        },
-      });
-    } else if (update.type === updateTypes.DELETE_ACCOUNT) {
-      for (const threadID in threadInfos) {
-        const threadInfo = threadInfos[threadID];
-        const newMembers = threadInfo.members.filter(
-          member => member.id !== update.deletedUserID,
-        );
-        if (newMembers.length < threadInfo.members.length) {
-          const updatedThread = {
-            ...threadInfo,
-            members: newMembers,
-          };
-          threadOperations.push({
-            type: 'replace',
-            payload: {
-              id: threadID,
-              threadInfo: updatedThread,
-            },
-          });
-        }
-      }
-    }
-  }
-  return threadOperations;
+  return payload.updatesResult.newUpdates
+    .map(update =>
+      updateSpecs[update.type].generateOpsForThreadUpdates?.(
+        threadInfos,
+        update,
+      ),
+    )
+    .filter(Boolean)
+    .flat();
 }
 
 function findInconsistencies(
diff --git a/lib/shared/updates/bad-device-token-spec.js b/lib/shared/updates/bad-device-token-spec.js
--- a/lib/shared/updates/bad-device-token-spec.js
+++ b/lib/shared/updates/bad-device-token-spec.js
@@ -1,5 +1,7 @@
 // @flow
 
 import type { UpdateSpec } from './update-spec.js';
+import type { BadDeviceTokenUpdateInfo } from '../../types/update-types.js';
 
-export const badDeviceTokenSpec: UpdateSpec = Object.freeze({});
+export const badDeviceTokenSpec: UpdateSpec<BadDeviceTokenUpdateInfo> =
+  Object.freeze({});
diff --git a/lib/shared/updates/delete-account-spec.js b/lib/shared/updates/delete-account-spec.js
--- a/lib/shared/updates/delete-account-spec.js
+++ b/lib/shared/updates/delete-account-spec.js
@@ -1,5 +1,35 @@
 // @flow
 
 import type { UpdateSpec } from './update-spec.js';
+import type { RawThreadInfos } from '../../types/thread-types.js';
+import type { AccountDeletionUpdateInfo } from '../../types/update-types.js';
 
-export const deleteAccountSpec: UpdateSpec = Object.freeze({});
+export const deleteAccountSpec: UpdateSpec<AccountDeletionUpdateInfo> =
+  Object.freeze({
+    generateOpsForThreadUpdates(
+      storeThreadInfos: RawThreadInfos,
+      update: AccountDeletionUpdateInfo,
+    ) {
+      const operations = [];
+      for (const threadID in storeThreadInfos) {
+        const threadInfo = storeThreadInfos[threadID];
+        const newMembers = threadInfo.members.filter(
+          member => member.id !== update.deletedUserID,
+        );
+        if (newMembers.length < threadInfo.members.length) {
+          const updatedThread = {
+            ...threadInfo,
+            members: newMembers,
+          };
+          operations.push({
+            type: 'replace',
+            payload: {
+              id: threadID,
+              threadInfo: updatedThread,
+            },
+          });
+        }
+      }
+      return operations;
+    },
+  });
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
@@ -1,5 +1,25 @@
 // @flow
 
 import type { UpdateSpec } from './update-spec.js';
+import type { RawThreadInfos } from '../../types/thread-types.js';
+import type { ThreadDeletionUpdateInfo } from '../../types/update-types.js';
 
-export const deleteThreadSpec: UpdateSpec = Object.freeze({});
+export const deleteThreadSpec: UpdateSpec<ThreadDeletionUpdateInfo> =
+  Object.freeze({
+    generateOpsForThreadUpdates(
+      storeThreadInfos: RawThreadInfos,
+      update: ThreadDeletionUpdateInfo,
+    ) {
+      if (storeThreadInfos[update.threadID]) {
+        return [
+          {
+            type: 'remove',
+            payload: {
+              ids: [update.threadID],
+            },
+          },
+        ];
+      }
+      return null;
+    },
+  });
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
@@ -1,5 +1,27 @@
 // @flow
 
+import _isEqual from 'lodash/fp/isEqual.js';
+
 import type { UpdateSpec } from './update-spec.js';
+import type { RawThreadInfos } from '../../types/thread-types.js';
+import type { ThreadJoinUpdateInfo } from '../../types/update-types.js';
 
-export const joinThreadSpec: UpdateSpec = Object.freeze({});
+export const joinThreadSpec: UpdateSpec<ThreadJoinUpdateInfo> = Object.freeze({
+  generateOpsForThreadUpdates(
+    storeThreadInfos: RawThreadInfos,
+    update: ThreadJoinUpdateInfo,
+  ) {
+    if (_isEqual(storeThreadInfos[update.threadInfo.id])(update.threadInfo)) {
+      return null;
+    }
+    return [
+      {
+        type: 'replace',
+        payload: {
+          id: update.threadInfo.id,
+          threadInfo: update.threadInfo,
+        },
+      },
+    ];
+  },
+});
diff --git a/lib/shared/updates/update-current-user-spec.js b/lib/shared/updates/update-current-user-spec.js
--- a/lib/shared/updates/update-current-user-spec.js
+++ b/lib/shared/updates/update-current-user-spec.js
@@ -1,5 +1,7 @@
 // @flow
 
 import type { UpdateSpec } from './update-spec.js';
+import type { CurrentUserUpdateInfo } from '../../types/update-types.js';
 
-export const updateCurrentUserSpec: UpdateSpec = Object.freeze({});
+export const updateCurrentUserSpec: UpdateSpec<CurrentUserUpdateInfo> =
+  Object.freeze({});
diff --git a/lib/shared/updates/update-entry-spec.js b/lib/shared/updates/update-entry-spec.js
--- a/lib/shared/updates/update-entry-spec.js
+++ b/lib/shared/updates/update-entry-spec.js
@@ -1,5 +1,6 @@
 // @flow
 
 import type { UpdateSpec } from './update-spec.js';
+import type { EntryUpdateInfo } from '../../types/update-types.js';
 
-export const updateEntrySpec: UpdateSpec = Object.freeze({});
+export const updateEntrySpec: UpdateSpec<EntryUpdateInfo> = Object.freeze({});
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
@@ -1,3 +1,12 @@
 // @flow
 
-export type UpdateSpec = {};
+import type { ThreadStoreOperation } from '../../ops/thread-store-ops.js';
+import type { RawThreadInfos } from '../../types/thread-types.js';
+import type { ClientUpdateInfo } from '../../types/update-types.js';
+
+export type UpdateSpec<UpdateInfo: ClientUpdateInfo> = {
+  +generateOpsForThreadUpdates?: (
+    storeThreadInfos: RawThreadInfos,
+    update: UpdateInfo,
+  ) => ?$ReadOnlyArray<ThreadStoreOperation>,
+};
diff --git a/lib/shared/updates/update-specs.js b/lib/shared/updates/update-specs.js
--- a/lib/shared/updates/update-specs.js
+++ b/lib/shared/updates/update-specs.js
@@ -13,7 +13,7 @@
 import { updateTypes, type UpdateType } from '../../types/update-types-enum.js';
 
 export const updateSpecs: {
-  +[UpdateType]: UpdateSpec,
+  +[UpdateType]: UpdateSpec<*>,
 } = Object.freeze({
   [updateTypes.DELETE_ACCOUNT]: deleteAccountSpec,
   [updateTypes.UPDATE_THREAD]: updateThreadSpec,
diff --git a/lib/shared/updates/update-thread-read-status-spec.js b/lib/shared/updates/update-thread-read-status-spec.js
--- a/lib/shared/updates/update-thread-read-status-spec.js
+++ b/lib/shared/updates/update-thread-read-status-spec.js
@@ -1,5 +1,40 @@
 // @flow
 
 import type { UpdateSpec } from './update-spec.js';
+import type {
+  RawThreadInfo,
+  RawThreadInfos,
+} from '../../types/thread-types.js';
+import type { ThreadReadStatusUpdateInfo } from '../../types/update-types.js';
 
-export const updateThreadReadStatusSpec: UpdateSpec = Object.freeze({});
+export const updateThreadReadStatusSpec: UpdateSpec<ThreadReadStatusUpdateInfo> =
+  Object.freeze({
+    generateOpsForThreadUpdates(
+      storeThreadInfos: RawThreadInfos,
+      update: ThreadReadStatusUpdateInfo,
+    ) {
+      const storeThreadInfo: ?RawThreadInfo = storeThreadInfos[update.threadID];
+      if (
+        !storeThreadInfo ||
+        storeThreadInfo.currentUser.unread === update.unread
+      ) {
+        return null;
+      }
+      const updatedThread = {
+        ...storeThreadInfo,
+        currentUser: {
+          ...storeThreadInfo.currentUser,
+          unread: update.unread,
+        },
+      };
+      return [
+        {
+          type: 'replace',
+          payload: {
+            id: update.threadID,
+            threadInfo: updatedThread,
+          },
+        },
+      ];
+    },
+  });
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
@@ -1,5 +1,27 @@
 // @flow
 
+import _isEqual from 'lodash/fp/isEqual.js';
+
 import type { UpdateSpec } from './update-spec.js';
+import type { RawThreadInfos } from '../../types/thread-types.js';
+import type { ThreadUpdateInfo } from '../../types/update-types.js';
 
-export const updateThreadSpec: UpdateSpec = Object.freeze({});
+export const updateThreadSpec: UpdateSpec<ThreadUpdateInfo> = Object.freeze({
+  generateOpsForThreadUpdates(
+    storeThreadInfos: RawThreadInfos,
+    update: ThreadUpdateInfo,
+  ) {
+    if (_isEqual(storeThreadInfos[update.threadInfo.id])(update.threadInfo)) {
+      return null;
+    }
+    return [
+      {
+        type: 'replace',
+        payload: {
+          id: update.threadInfo.id,
+          threadInfo: update.threadInfo,
+        },
+      },
+    ];
+  },
+});
diff --git a/lib/shared/updates/update-user-spec.js b/lib/shared/updates/update-user-spec.js
--- a/lib/shared/updates/update-user-spec.js
+++ b/lib/shared/updates/update-user-spec.js
@@ -1,5 +1,6 @@
 // @flow
 
 import type { UpdateSpec } from './update-spec.js';
+import type { UserUpdateInfo } from '../../types/update-types.js';
 
-export const updateUserSpec: UpdateSpec = Object.freeze({});
+export const updateUserSpec: UpdateSpec<UserUpdateInfo> = Object.freeze({});
diff --git a/lib/types/update-types.js b/lib/types/update-types.js
--- a/lib/types/update-types.js
+++ b/lib/types/update-types.js
@@ -174,7 +174,7 @@
   | CurrentUserRawUpdateInfo
   | UserRawUpdateInfo;
 
-type AccountDeletionUpdateInfo = {
+export type AccountDeletionUpdateInfo = {
   +type: 0,
   +id: string,
   +time: number,
@@ -188,7 +188,7 @@
     deletedUserID: t.String,
   });
 
-type ThreadUpdateInfo = {
+export type ThreadUpdateInfo = {
   +type: 1,
   +id: string,
   +time: number,
@@ -201,7 +201,7 @@
     time: t.Number,
     threadInfo: rawThreadInfoValidator,
   });
-type ThreadReadStatusUpdateInfo = {
+export type ThreadReadStatusUpdateInfo = {
   +type: 2,
   +id: string,
   +time: number,
@@ -216,7 +216,7 @@
     threadID: tID,
     unread: t.Boolean,
   });
-type ThreadDeletionUpdateInfo = {
+export type ThreadDeletionUpdateInfo = {
   +type: 3,
   +id: string,
   +time: number,
@@ -230,7 +230,7 @@
     threadID: tID,
   });
 
-type ThreadJoinUpdateInfo = {
+export type ThreadJoinUpdateInfo = {
   +type: 4,
   +id: string,
   +time: number,
@@ -249,7 +249,7 @@
     truncationStatus: messageTruncationStatusValidator,
     rawEntryInfos: t.list(rawEntryInfoValidator),
   });
-type BadDeviceTokenUpdateInfo = {
+export type BadDeviceTokenUpdateInfo = {
   +type: 5,
   +id: string,
   +time: number,
@@ -262,7 +262,7 @@
     time: t.Number,
     deviceToken: t.String,
   });
-type EntryUpdateInfo = {
+export type EntryUpdateInfo = {
   +type: 6,
   +id: string,
   +time: number,
@@ -275,13 +275,13 @@
     time: t.Number,
     entryInfo: rawEntryInfoValidator,
   });
-type CurrentUserUpdateInfo = {
+export type CurrentUserUpdateInfo = {
   +type: 7,
   +id: string,
   +time: number,
   +currentUserInfo: LoggedInUserInfo,
 };
-type UserUpdateInfo = {
+export type UserUpdateInfo = {
   +type: 8,
   +id: string,
   +time: number,