fix(web): address review — shared types and stable optimistic data

- Extract ToggleCommentReactionVars and ToggleIssueReactionVars shared
  types so mutation definitions and useMutationState consumers stay in
  sync without as-casts on inline types
- Replace new Date().toISOString() with empty string in optimistic
  reaction objects to avoid unstable references in useMemo

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-04-08 15:41:16 +08:00
parent 060afc848c
commit 17e37ec4db
3 changed files with 24 additions and 18 deletions

View file

@ -11,6 +11,22 @@ import type {
} from "@/shared/types";
import type { TimelineEntry, IssueSubscriber, Reaction } from "@/shared/types";
// ---------------------------------------------------------------------------
// Shared mutation variable types — used by both mutation hooks and
// useMutationState consumers to keep the type assertion in sync.
// ---------------------------------------------------------------------------
export type ToggleCommentReactionVars = {
commentId: string;
emoji: string;
existing: Reaction | undefined;
};
export type ToggleIssueReactionVars = {
emoji: string;
existing: IssueReaction | undefined;
};
// ---------------------------------------------------------------------------
// Done issue pagination
// ---------------------------------------------------------------------------
@ -339,11 +355,7 @@ export function useToggleCommentReaction(issueId: string) {
commentId,
emoji,
existing,
}: {
commentId: string;
emoji: string;
existing: Reaction | undefined;
}) => {
}: ToggleCommentReactionVars) => {
if (existing) {
await api.removeReaction(commentId, emoji);
return null;
@ -367,10 +379,7 @@ export function useToggleIssueReaction(issueId: string) {
mutationFn: async ({
emoji,
existing,
}: {
emoji: string;
existing: IssueReaction | undefined;
}) => {
}: ToggleIssueReactionVars) => {
if (existing) {
await api.removeIssueReaction(issueId, emoji);
return null;

View file

@ -8,7 +8,7 @@ import type {
IssueReactionRemovedPayload,
} from "@/shared/types";
import { issueReactionsOptions, issueKeys } from "@core/issues/queries";
import { useToggleIssueReaction } from "@core/issues/mutations";
import { useToggleIssueReaction, type ToggleIssueReactionVars } from "@core/issues/mutations";
import { useWSEvent, useWSReconnect } from "@/features/realtime";
export function useIssueReactions(issueId: string, userId?: string) {
@ -80,9 +80,7 @@ export function useIssueReactions(issueId: string, userId?: string) {
status: "pending",
},
select: (m) =>
m.state.variables as
| { emoji: string; existing: IssueReaction | undefined }
| undefined,
m.state.variables as ToggleIssueReactionVars | undefined,
});
const reactions = useMemo(() => {
@ -111,7 +109,7 @@ export function useIssueReactions(issueId: string, userId?: string) {
actor_type: "member",
actor_id: userId ?? "",
emoji: vars.emoji,
created_at: new Date().toISOString(),
created_at: "",
},
];
}

View file

@ -17,6 +17,7 @@ import {
useUpdateComment,
useDeleteComment,
useToggleCommentReaction,
type ToggleCommentReactionVars,
} from "@core/issues/mutations";
import { useWSEvent, useWSReconnect } from "@/features/realtime";
import { toast } from "sonner";
@ -269,9 +270,7 @@ export function useIssueTimeline(issueId: string, userId?: string) {
status: "pending",
},
select: (m) =>
m.state.variables as
| { commentId: string; emoji: string; existing: Reaction | undefined }
| undefined,
m.state.variables as ToggleCommentReactionVars | undefined,
});
const optimisticTimeline = useMemo(() => {
@ -306,7 +305,7 @@ export function useIssueTimeline(issueId: string, userId?: string) {
actor_type: "member",
actor_id: userId ?? "",
emoji: vars.emoji,
created_at: new Date().toISOString(),
created_at: "",
},
];
}