拖曳與平移的暫存狀態放 ref 而非 state
互動進行中需要紀錄「起始點、基準偏移、目前是哪個物件被拖」這類資料,這些資料不需要觸發畫面重繪,放 useRef 而非 useState。每秒幾十次的 pointermove 才不會炸 re-render。
做法
const panStateRef = useRef<{ startX: number; startY: number; baseOffsetX: number; baseOffsetY: number } | null>(null)
const dragStateRef = useRef<{ id: string; startWorld: Point; baseX: number; baseY: number } | null>(null)
const drawStateRef = useRef<{ kind: 'pen' | 'rect' | 'ellipse'; startWorld: Point } | null>(null)
const onPointerDown = (e) => {
panStateRef.current = { startX: e.clientX, ..., baseOffsetX: viewport.offsetX, ... }
}
const onPointerMove = (e) => {
if (panStateRef.current) {
const { startX, baseOffsetX } = panStateRef.current
setViewport(vp => ({ ...vp, offsetX: baseOffsetX + (e.clientX - startX) }))
}
}
const onPointerUp = () => {
panStateRef.current = null
}判斷標準:這份資料變了,畫面要重畫嗎?
- 要重畫 →
useState(如 viewport、items、目前選取的 id) - 不重畫,只是過程紀錄 →
useRef(如 startX、基準偏移、目前是哪個物件正在被拖)
把所有東西都塞 useState 是新手最常見的效能問題:每秒 60+ 次 move event,每次都 setState → 每次都 re-render 整個畫布。
為什麼「基準值」要在 down 時存起來
// 錯:每次 move 都用 viewport.offsetX 當基準 → 結果是位移會累積/抖動
setViewport(vp => ({ ...vp, offsetX: vp.offsetX + (e.clientX - lastX) }))
// 對:down 時鎖定基準,move 時算「相對於基準的總位移」
setViewport(vp => ({ ...vp, offsetX: baseOffsetX + (e.clientX - startX) }))後者的 e.clientX - startX 永遠是「從按下到現在的總位移」,再加到「按下時的基準」,數學乾淨、不會累積誤差。