Flip with [data-] attribute
import { useLayoutEffect, useRef, useState } from 'react';
function useFlipList<T>(deps: T) {
const containerRef = useRef<HTMLElement | null>(null);
const positionsRef = useRef<Map<string, number> | null>(null);
// 순서를 바꾸기 직전에 호출 (setState 전에)
const capture = () => {
const container = containerRef.current;
if (!container) return;
const positions = new Map<string, number>();
container.querySelectorAll<HTMLElement>('[data-row-id]').forEach((el) => {
positions.set(el.dataset.rowId ?? '', el.getBoundingClientRect().top);
});
positionsRef.current = positions;
};
useLayoutEffect(() => {
const previous = positionsRef.current;
if (!previous) return;
positionsRef.current = null;
const container = containerRef.current;
if (!container) return;
container.querySelectorAll<HTMLElement>('[data-row-id]').forEach((el) => {
const before = previous.get(el.dataset.rowId ?? '');
if (before === undefined) return;
const delta = before - el.getBoundingClientRect().top;
if (delta === 0) return;
el.animate(
[{ transform: `translateY(${delta}px)` }, { transform: 'translateY(0)' }],
{ duration: 200, easing: 'ease' }
);
});
}, [deps]);
return { containerRef, capture };
}
const swap = <T,>(arr: T[], a: number, b: number): T[] => {
if (b < 0 || b >= arr.length) return arr;
const next = [...arr];
[next[a], next[b]] = [next[b], next[a]];
return next;
};
function SortableList() {
const [items, setItems] = useState([
{ id: '1', name: '재질' },
{ id: '2', name: '치아위치' },
{ id: '3', name: '제품군' },
]);
const { containerRef, capture } = useFlipList(items);
const move = (index: number, dir: -1 | 1) => {
capture();
setItems((prev) => swap(prev, index, index + dir));
};
return (
<ul ref={(el) => (containerRef.current = el)}>
{items.map((item, i) => (
<li key={item.id} data-row-id={item.id}>
{item.name}
<button onClick={() => move(i, -1)}>↑</button>
<button onClick={() => move(i, 1)}>↓</button>
</li>
))}
</ul>
);
}