···22import { type DragEndEvent, useDndMonitor } from "@dnd-kit/core";
33import { useSuspenseQuery } from "@tanstack/react-query";
44import { createFileRoute, Link } from "@tanstack/react-router";
55-import { useCallback, useMemo, useState } from "react";
55+import { useCallback, useMemo, useRef, useState } from "react";
66import { ErrorBoundary } from "react-error-boundary";
77import { toast } from "sonner";
88import { CardDragOverlay } from "@/components/deck/CardDragOverlay";
···147147 );
148148 const deck = deckRecord.deck;
149149150150+ // Ref to always have fresh deck state (avoids stale closures in toast callbacks)
151151+ const deckRef = useRef(deck);
152152+ deckRef.current = deck;
153153+150154 const [groupBy, setGroupBy] = usePersistedState<GroupBy>(
151155 "deckbelcher:viewConfig:groupBy",
152156 "typeAndTags",
···205209 );
206210207211 // Helper to update deck via mutation
208208- const updateDeck = async (updater: (prev: Deck) => Deck) => {
209209- if (!isOwner) return;
210210- const updated = updater(deck);
211211- await mutation.mutateAsync(updated);
212212- };
212212+ // Uses deckRef to avoid stale closures in async callbacks (e.g., toast undo)
213213+ const updateDeck = useCallback(
214214+ async (updater: (prev: Deck) => Deck) => {
215215+ if (!isOwner) return;
216216+ const updated = updater(deckRef.current);
217217+ await mutation.mutateAsync(updated);
218218+ },
219219+ [isOwner, mutation],
220220+ );
213221214222 // Highlight cards that were changed - clear after paint so it can trigger again
215223 const handleCardsChanged = (changedIds: Set<ScryfallId>) => {
···288296 // Update to success with undo action
289297 toast.success("Card removed from deck", {
290298 id: toastId,
299299+ duration: 10000,
291300 action: {
292301 label: "Undo",
293302 onClick: () => {
294303 toast.promise(
295295- updateDeck((prev) => ({
296296- ...prev,
297297- cards: [...prev.cards, cardToDelete],
298298- })),
304304+ updateDeck((prev) =>
305305+ addCardToDeck(
306306+ prev,
307307+ cardToDelete.scryfallId,
308308+ cardToDelete.oracleId,
309309+ cardToDelete.section as Section,
310310+ cardToDelete.quantity,
311311+ cardToDelete.tags ?? [],
312312+ ),
313313+ ),
299314 {
300315 loading: "Undoing...",
301316 success: "Card restored",
···381396 if (cardToDelete) {
382397 toast.success("Card removed from deck", {
383398 id: toastId,
399399+ duration: 10000,
384400 action: {
385401 label: "Undo",
386402 onClick: () => {
387387- // Re-insert the exact card that was deleted
388403 toast.promise(
389389- updateDeck((prev) => ({
390390- ...prev,
391391- cards: [...prev.cards, cardToDelete],
392392- })),
404404+ updateDeck((prev) =>
405405+ addCardToDeck(
406406+ prev,
407407+ cardToDelete.scryfallId,
408408+ cardToDelete.oracleId,
409409+ cardToDelete.section as Section,
410410+ cardToDelete.quantity,
411411+ cardToDelete.tags ?? [],
412412+ ),
413413+ ),
393414 {
394415 loading: "Undoing...",
395416 success: "Card restored",
+7-5
todos.md
···8899## Bugs
10101111-### Delete undo adds N+1 copies
1212-- **Location**: Deck editor undo logic
1313-- **Issue**: Undoing a card deletion adds N+1 of the card as independent copies instead of restoring the original single entry
1414-- **Repro**: Delete a card with qty 4, undo, observe 5 separate entries
1515-1611### Bare regex for name search doesn't work
1712- **Location**: `src/lib/search/parser.ts`, `parseNameExpr()`
1813- **Issue**: `/goblin.*king/i` syntax is parsed but not matched correctly for bare name searches (works in field values like `o:/regex/`)
···7368## Refactoring (Technical Debt)
74697570### High Priority
7171+7272+#### Deck editor: Replace ref with reducer pattern
7373+- **Location**: `src/routes/profile/$did/deck/$rkey/index.tsx`
7474+- **Issue**: Currently uses `deckRef` to avoid stale closures in toast undo callbacks. This is a band-aid fix.
7575+- **Better approach**: Use `useReducer` for local deck state with explicit actions (`ADD_CARD`, `REMOVE_CARD`, etc.)
7676+- **Benefits**: Natural fit for undo/redo (action history), integrates well with Immer, clearer mental model
7777+- **Effort**: Medium
76787779#### Reduce computeManaSymbolsVsSources complexity
7880- **Location**: `src/lib/deck-stats.ts:327-502` (176 lines)