React’te Animasyonları “Layout” Seviyesinde Yönetmek: FLIP Tekniği ile Akıcı Liste Geçişleri
FLIP tekniğiyle React’te liste sıralama/ekleme/silme animasyonlarını düşük maliyetle ve takılmadan uygulayın.
React’te bir listeyi filtrelediğinizde, sıraladığınızda veya eleman ekleyip sildiğinizde yaşanan “zıplama” hissi çoğu zaman animasyonun yanlış yerde yapılmasından kaynaklanır. CSS transition ile height/top gibi maliyetli özellikleri oynatmak yerine, FLIP (First–Last–Invert–Play) tekniğiyle değişimi “layout” yerine transform üzerinden akıcı hale getirebilirsiniz.
FLIP nedir?
FLIP dört adımda çalışır:
- First: Değişiklikten önce elemanların konumlarını ölç.
- Last: React ile state’i değiştir, DOM yeni konumlara gelsin.
- Invert: Yeni konum ile eski konum arasındaki farkı hesapla ve elemanı ters yönde
transformile eski yerine “geri taşı”. - Play:
transform’u animasyonla sıfıra getir; kullanıcı akıcı geçiş görür.
Bu yaklaşım, tarayıcının pahalı layout hesaplarını minimuma indirip GPU dostu transform animasyonu kullanır.
Örnek: Sıralanabilir bir listeyi FLIP ile canlandırmak
Aşağıdaki örnekte, butona basınca liste sıralaması değişiyor. FLIP ile tüm elemanlar yeni yerine “kayarak” gider.
import React, { useLayoutEffect, useRef, useState } from "react";
function useFlip(keys) {
const rectsRef = useRef(new Map());
const nodesRef = useRef(new Map());
const register = (key) => (node) => {
if (!node) {
nodesRef.current.delete(key);
return;
}
nodesRef.current.set(key, node);
};
// First: render öncesi mevcut konumları sakla
useLayoutEffect(() => {
rectsRef.current.clear();
for (const key of keys) {
const node = nodesRef.current.get(key);
if (node) rectsRef.current.set(key, node.getBoundingClientRect());
}
});
// Last + Invert + Play: DOM güncellendikten sonra farkı uygula
useLayoutEffect(() => {
for (const key of keys) {
const node = nodesRef.current.get(key);
const first = rectsRef.current.get(key);
if (!node || !first) continue;
const last = node.getBoundingClientRect();
const dx = first.left - last.left;
const dy = first.top - last.top;
if (dx === 0 && dy === 0) continue;
node.style.transform = `translate(${dx}px, ${dy}px)`;
node.style.transition = "transform 0s";
// next frame: animasyonu başlat
requestAnimationFrame(() => {
node.style.transition = "transform 220ms cubic-bezier(.2,.8,.2,1)";
node.style.transform = "translate(0px, 0px)";
});
}
}, [keys.join("|")]);
return { register };
}
export default function FlipList() {
const [items, setItems] = useState([
{ id: "a", label: "React" },
{ id: "b", label: "Vue" },
{ id: "c", label: "Svelte" },
{ id: "d", label: "Solid" },
]);
const keys = items.map((x) => x.id);
const { register } = useFlip(keys);
return (
<div style={{ maxWidth: 360 }}>
<button
onClick={() =>
setItems((prev) => [...prev].reverse())
}
>
Sırayı ters çevir
</button>
<ul style={{ listStyle: "none", padding: 0, marginTop: 12 }}>
{items.map((item) => (
<li
key={item.id}
ref={register(item.id)}
style={{
padding: "10px 12px",
marginBottom: 8,
border: "1px solid #e5e7eb",
borderRadius: 8,
background: "white",
willChange: "transform",
}}
>
{item.label}
</li>
))}
</ul>
</div>
);
}
Neden useLayoutEffect?
Ölçüm ve transform “tersleme” işlemi, tarayıcı ekrana boyamadan önce yapılırsa flicker (bir frame’lik sıçrama) azalır. Bu yüzden useEffect yerine useLayoutEffect tercih edilir.
Pratik ipuçları
- Sadece transform/opacity animasyonu yapın:
height,top,leftgibi özellikler daha pahalıdır. - Elemanlara
will-change: transformeklemek, sık animasyonlarda yardımcı olabilir. - “Silme” animasyonu istiyorsanız, elemanı state’ten hemen kaldırmak yerine kısa süre DOM’da tutup sonra kaldırmayı (exit animation) düşünün.
- Büyük listelerde ölçüm maliyetini azaltmak için sadece “değişen” aralığı ölçmeye çalışın.
Ne zaman kullanmalı?
- Drag & drop sıralama (kütüphane kullanmasanız bile),
- Filtreleme sonrası liste yeniden dizilimi,
- Kanban kolonlarında kartların yer değiştirmesi,
- Grid/galeri düzeninde elemanların akıcı geçişi.
FLIP, React’te animasyonu “sonradan eklenen makyaj” olmaktan çıkarıp etkileşimin bir parçası haline getirir: daha az jank, daha çok akıcılık.