background
In the list development, if we need to delete an item in the list, we usually delete the item and then re-update the list and render the list, which will have a blunt feeling, as follows:
Effect of initial
In GIF, after deleting the box_6 list item, the following list items are directly replaced in place. At this time, if you want to delete an item in the list, the following module over the process of transition animation, this will be much smoother
Basic code
// index.js
import React, { useState } from "react";
import { v4 as uuidv4 } from "uuid";
import "./style.css";
const data = Array.from({ length: 10 }, (_, index) = > ({
text: `box_${index + 1}`.id: uuidv4()
}));
export default function App() {
const [list, setList] = useState(data);
const handleDeleteClick = index= > {
setList(list.slice(0, index).concat(list.slice(index + 1)));
};
return (
<div className="card-list">
{list.map((item, index) => {
return (
<ListItem
key={item.id}
item={item}
index={index}
handleDeleteClick={handleDeleteClick}
/>
);
})}
</div>
);
}
export const ListItem = ({ item, index, handleDeleteClick }) = > {
const itemRef = useRef(null);
return (
<div className="card" ref={itemRef}>
<div className="del" onClick={()= > handleDeleteClick(index)} />
{item.text}
</div>
);
};
Copy the code
Want to effect
How to implement
Problem analysis
When an item in the list is deleted, it triggers a rerendering of the list. Since the list item is rendered with a unique UUID as the key of the list item, according to the React DOM Diff rule, the components of the list item before the deletion do not change because the key does not change. However, the key of the following list items has changed in dislocation, so only one deletion operation will be performed and then the number of list items will be moved. The construction performance of the Virtual DOM will be better, but the mounting process of React Virtual DOM into real DOM will still be redrawn. So the whole process of change can seem rather blunt;
How to solve
Record the position of each item in the list relative to the list before and after deletion, and add the Transform animation. The animation time is less than the time interval before and after deletion. The main problem is, how to record the relative position of each item in the list before and after deletion
Code changes
import React, { useState, useEffect, useLayoutEffect, useRef } from "react";
import { v4 as uuidv4 } from "uuid";
import "./style.css";
const data = Array.from({ length: 10 }, (_, index) = > ({
text: `box_${index + 1}`.id: uuidv4()
}));
export default function App() {
const [list, setList] = useState(data);
const listRef = useRef(null);
const handleDeleteClick = index= > {
setList(list.slice(0, index).concat(list.slice(index + 1)));
};
return (
<div className="card-list" ref={listRef}>
{list.map((item, index) => {
return (
<ListItem
key={item.id}
item={item}
index={index}
handleDeleteClick={handleDeleteClick}
listRef={listRef}
/>
);
})}
</div>
);
}
const useSimpleFlip = ({ ref, infoInit = null, infoFn, effectFn }, deps) = > {
const infoRef = useRef(
typeof infoInit === "function" ? infoInit() : infoInit
);
// useLayoutEffect hook Records the relative position of the list item before render relative to the list container after each delete action
// useOnceEffect fixes the initial relative position record of a list item after first rendering
useLayoutEffect(() = > {
const prevInfo = infoRef.current;
const nextInfo = infoFn(ref, { prevInfo, infoRef });
const res = effectFn(ref, { prevInfo, nextInfo, infoRef });
infoRef.current = nextInfo;
return res;
}, deps);
useOnceEffect(
() = > {
infoRef.current = infoFn(ref, { prevInfo: infoRef.current, infoRef });
},
true,
deps
);
};
const useOnceEffect = (effect, condition, deps) = > {
const [once, setOnce] = useState(false);
useEffect(() = > {
if(condition && ! once) { effect(); setOnce(true);
}
}, deps);
return once;
};
export const ListItem = ({ item, index, handleDeleteClick, listRef }) = > {
const itemRef = useRef(null);
useSimpleFlip(
{
infoInit: null.ref: itemRef,
infoFn: r= > {
if (r.current && listRef.current) {
return {
position: {
left:
r.current.getBoundingClientRect().left -
listRef.current.getBoundingClientRect().left,
top:
r.current.getBoundingClientRect().top -
listRef.current.getBoundingClientRect().top
}
};
} else {
return null; }},effectFn: (r, { nextInfo, prevInfo }) = > {
if (prevInfo && nextInfo) {
const translateX = prevInfo.position.left - nextInfo.position.left;
const translateY = prevInfo.position.top - nextInfo.position.top;
const a = r.current.animate(
[
{ transform: `translate(${translateX}px, ${translateY}px)` },
{ transform: "translate(0, 0)"}, {duration: 300.easing: "ease"});return () = > a && a.cancel();
}
}
},
[index]
);
return (
<div className="card" ref={itemRef}>
<div className="del" onClick={()= > handleDeleteClick(index)} />
{item.text}
</div>
);
};
Copy the code
Applicable scenario
- Delete and insert waterfall list without knowing the size of list card in advance, everything is calculated by react DOM;