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;