Skip to content

Commit 8731447

Browse files
adrians5jclaude
andauthored
refactor(collaboration, notifications): name the presenters' state updates instead of runInAction (#5997)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 25cddf7 commit 8731447

2 files changed

Lines changed: 86 additions & 58 deletions

File tree

‎packages/collaboration/src/admin/presentation/comments/CommentsPresenter.ts‎

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { makeAutoObservable, runInAction, toJS } from "mobx";
1+
import { makeAutoObservable, toJS } from "mobx";
22
import { CommentsPresenter as PresenterAbstraction } from "./abstractions.js";
33
import type { ICreateThreadParams } from "./abstractions.js";
44
import { CollaborationApi } from "~/admin/features/api/abstractions.js";
@@ -134,9 +134,7 @@ class CommentsPresenterImpl implements PresenterAbstraction.Interface {
134134
if (!this.composerBody.trim() || this.composerSubmitting) {
135135
return;
136136
}
137-
runInAction(() => {
138-
this.composerSubmitting = true;
139-
});
137+
this.setComposerSubmitting(true);
140138
try {
141139
// Empty locator => entry-level (unanchored) comment.
142140
await this.createThread({
@@ -146,20 +144,15 @@ class CommentsPresenterImpl implements PresenterAbstraction.Interface {
146144
});
147145
this.resetComposer();
148146
} finally {
149-
runInAction(() => {
150-
this.composerSubmitting = false;
151-
});
147+
this.setComposerSubmitting(false);
152148
}
153149
}
154150

155151
async init(contentId: string) {
156152
if (this.loadedContentId === contentId) {
157153
return;
158154
}
159-
runInAction(() => {
160-
this.contentId = contentId;
161-
this.loadedContentId = contentId;
162-
});
155+
this.setContentId(contentId);
163156
void this.loadUsers();
164157
await this.reload();
165158
}
@@ -170,9 +163,7 @@ class CommentsPresenterImpl implements PresenterAbstraction.Interface {
170163
}
171164
try {
172165
const users = await this.api.listMentionableUsers();
173-
runInAction(() => {
174-
this.users = users;
175-
});
166+
this.setUsers(users);
176167
} catch {
177168
// Best-effort — mentions simply won't autocomplete.
178169
}
@@ -182,25 +173,16 @@ class CommentsPresenterImpl implements PresenterAbstraction.Interface {
182173
if (!this.contentId) {
183174
return;
184175
}
185-
runInAction(() => {
186-
this.loading = true;
187-
this.error = null;
188-
});
176+
this.startLoading();
189177
try {
190178
const result = await this.api.listThreads({
191179
contentType: CONTENT_TYPE_CMS_ENTRY,
192180
contentId: this.contentId
193181
});
194-
runInAction(() => {
195-
this.threads = result.items;
196-
this.loading = false;
197-
});
182+
this.finishLoading(result.items);
198183
} catch (err) {
199184
console.error("[collaboration] failed to load comment threads", err);
200-
runInAction(() => {
201-
this.error = readableError(err);
202-
this.loading = false;
203-
});
185+
this.failLoading(readableError(err));
204186
}
205187
}
206188

@@ -216,9 +198,7 @@ class CommentsPresenterImpl implements PresenterAbstraction.Interface {
216198
body: params.body,
217199
mentions: params.mentions
218200
});
219-
runInAction(() => {
220-
this.threads = [thread, ...this.threads];
221-
});
201+
this.prependThread(thread);
222202
}
223203

224204
async reply(threadId: string, body: string, mentions?: string[]) {
@@ -249,9 +229,7 @@ class CommentsPresenterImpl implements PresenterAbstraction.Interface {
249229

250230
async remove(threadId: string) {
251231
await this.api.deleteThread(threadId);
252-
runInAction(() => {
253-
this.threads = this.threads.filter(thread => thread.id !== threadId);
254-
});
232+
this.removeThread(threadId);
255233
}
256234

257235
async editMessage(threadId: string, messageId: string, body: string) {
@@ -275,12 +253,49 @@ class CommentsPresenterImpl implements PresenterAbstraction.Interface {
275253
await this.reload();
276254
}
277255

256+
/*
257+
* State changes that happen after an `await` go through these methods. makeAutoObservable makes
258+
* each of them an action, so the change runs inside one even though the caller has already
259+
* left its own action.
260+
*/
261+
private setComposerSubmitting(submitting: boolean) {
262+
this.composerSubmitting = submitting;
263+
}
264+
265+
private setContentId(contentId: string) {
266+
this.contentId = contentId;
267+
this.loadedContentId = contentId;
268+
}
269+
270+
private setUsers(users: CollabUser[]) {
271+
this.users = users;
272+
}
273+
274+
private startLoading() {
275+
this.loading = true;
276+
this.error = null;
277+
}
278+
279+
private finishLoading(threads: CollabThread[]) {
280+
this.threads = threads;
281+
this.loading = false;
282+
}
283+
284+
private failLoading(error: string) {
285+
this.error = error;
286+
this.loading = false;
287+
}
288+
289+
private prependThread(thread: CollabThread) {
290+
this.threads = [thread, ...this.threads];
291+
}
292+
293+
private removeThread(threadId: string) {
294+
this.threads = this.threads.filter(thread => thread.id !== threadId);
295+
}
296+
278297
private replaceThread(updated: CollabThread) {
279-
runInAction(() => {
280-
this.threads = this.threads.map(thread =>
281-
thread.id === updated.id ? updated : thread
282-
);
283-
});
298+
this.threads = this.threads.map(thread => (thread.id === updated.id ? updated : thread));
284299
}
285300
}
286301

‎packages/notifications/src/admin/presentation/notifications/NotificationsPresenter.ts‎

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { makeAutoObservable, runInAction, toJS } from "mobx";
1+
import { makeAutoObservable, toJS } from "mobx";
22
import { NotificationsPresenter as PresenterAbstraction } from "./abstractions.js";
33
import type { NotificationsTab } from "./abstractions.js";
44
import { NotificationsApi } from "~/admin/features/api/abstractions.js";
@@ -124,33 +124,19 @@ class NotificationsPresenterImpl implements PresenterAbstraction.Interface {
124124
// Switching tabs or filters quickly can leave an older request still in flight; it may
125125
// fill its own view's cache, but only the latest request writes the visible list.
126126
const requestId = ++this.latestRequestId;
127-
runInAction(() => {
128-
// Set either way: an older request that showed the loading state won't clear it,
129-
// because it's no longer the latest.
130-
this.loading = showLoading;
131-
this.error = null;
132-
});
127+
this.startLoading(showLoading);
133128
try {
134129
const result = await this.api.list({
135130
archived,
136131
read: unreadOnly ? false : undefined,
137132
limit: 50
138133
});
139-
runInAction(() => {
140-
this.cache.set(key, result.items);
141-
if (requestId === this.latestRequestId) {
142-
this.items = result.items;
143-
this.loading = false;
144-
}
145-
});
134+
this.storeItems(key, result.items, requestId);
146135
} catch (err) {
147136
if (requestId !== this.latestRequestId) {
148137
return;
149138
}
150-
runInAction(() => {
151-
this.error = (err as Error).message;
152-
this.loading = false;
153-
});
139+
this.failLoading((err as Error).message);
154140
}
155141
}
156142

@@ -184,13 +170,40 @@ class NotificationsPresenterImpl implements PresenterAbstraction.Interface {
184170
private async refreshCounts() {
185171
try {
186172
const counts = await this.api.counts();
187-
runInAction(() => {
188-
this.counts = counts;
189-
});
173+
this.setCounts(counts);
190174
} catch {
191175
// best-effort
192176
}
193177
}
178+
179+
/*
180+
* State changes that happen after an `await` go through these methods. makeAutoObservable makes
181+
* each of them an action, so the change runs inside one even though the caller has already
182+
* left its own action.
183+
*/
184+
private startLoading(showLoading: boolean) {
185+
// Set either way: an older request that showed the loading state won't clear it, because
186+
// it's no longer the latest.
187+
this.loading = showLoading;
188+
this.error = null;
189+
}
190+
191+
private storeItems(key: string, items: Notification[], requestId: number) {
192+
this.cache.set(key, items);
193+
if (requestId === this.latestRequestId) {
194+
this.items = items;
195+
this.loading = false;
196+
}
197+
}
198+
199+
private failLoading(error: string) {
200+
this.error = error;
201+
this.loading = false;
202+
}
203+
204+
private setCounts(counts: NotificationCounts) {
205+
this.counts = counts;
206+
}
194207
}
195208

196209
export const NotificationsPresenter = PresenterAbstraction.createImplementation({

0 commit comments

Comments
 (0)