Draft Pattern 繪製中的物件放獨立 state
正在繪製、尚未完成的物件(draft)跟已完成的物件(items)分開存。繪製過程中只更新 draft,鬆手時才併入 items;過短/無效的 draft 直接丟掉。
做法
const [items, setItems] = useState<Item[]>([])
const [draftStroke, setDraftStroke] = useState<StrokeItem | null>(null)
const [draftRect, setDraftRect] = useState<RectItem | null>(null)
// 繪製中:只動 draft,不碰 items
const onPointerMove = (e) => {
setDraftStroke(s => s ? { ...s, points: [...s.points, world] } : s)
}
// 鬆手:有效才併入 items
const onPointerUp = () => {
if (draftStroke && draftStroke.points.length > 1) {
setItems(prev => [...prev, draftStroke])
}
if (draftRect && draftRect.width > 4 && draftRect.height > 4) {
setItems(prev => [...prev, draftRect])
}
setDraftStroke(null)
setDraftRect(null)
}
// 渲染:把兩邊合起來畫
const renderItems = [...items, ...(draftRect ? [draftRect] : []), ...(draftStroke ? [draftStroke] : [])]為什麼分開
如果繪製中直接 setItems(prev => [...prev.slice(0, -1), { ...prev.at(-1)!, points: [...] }]):
- 每次 move 都複製整個 items 陣列 — 已完成物件多時 O(n) 拷貝很貴
- items 陣列頻繁變更會讓所有 memo / 子元件 invalidate
- 中斷繪製(如
pointercancel)要回滾 items,很容易留下髒資料
把 draft 拆出來:
- 繪製中只動 draft 那一筆,items 完全不動
- 中斷只要
setDraft(null),items 自然乾淨 - 鬆手才一次
setItems(prev => [...prev, draft]),全程只 push 一次
「有效才併入」過濾誤觸
if (draftStroke && draftStroke.points.length > 1) { ... }
if (draftRect && draftRect.width > 4 && draftRect.height > 4) { ... }使用者按一下沒拖就放開(誤觸)會產生:
- 0 個點的筆畫
- 0×0 的矩形
直接丟掉,避免清單塞滿透明的空物件。
適用場景
- 畫筆、筆畫
- 拖曳建立矩形、橢圓、連線
- 選取框(marquee)— 鬆手後 draft 變成「選取結果」而非新物件
- 任何「按下 → 拖動預覽 → 放開確認」的互動