Search code examples
javascriptreactjsreact-spring

How to animate the filtering of a list using useTransition in react-spring


I am trying to animate the transition of a list when filtered using the new useTransition hook changes in react-spring v9.x so that as list items are filtered the remaining items move to their new positions.

So far I have managed to get components in the list to fade in and out but the remaining components just jump to their new positions instantly once the fade out animation has completed. I have not been able to change this.

How do I animate the remaining components to smoothly move to their new locations?

Here is a code sandbox link to the current code.

You can see the jumping effect clearest if you type 'p' into search bar and watch the component with the name Plum jump up after a short delay.

App.js

import { useState } from "react";
import { useSpring, useTransition, animated } from "react-spring";

export default function App() {
  const [items, setItems] = useState([
    { name: "Apple", key: 1 },
    { name: "Banana", key: 2 },
    { name: "Orange", key: 3 },
    { name: "Kiwifruit", key: 4 },
    { name: "Plum", key: 5 }
  ]);

  const [searchText, setSearchText] = useState("");

  const filteredItems = items.filter((item) =>
    item.name.toLowerCase().includes(searchText.toLowerCase())
  );

  const transition = useTransition(filteredItems, {
    from: { opacity: 0 },
    enter: { opacity: 1 },
    leave: { opacity: 0 }
  });

  const fadeInListItems = transition((style, item) => {
    return (
      <animated.div style={style}>
        <Item data={item} />
      </animated.div>
    );
  });

  const handleSearchBarChange = ({ target }) => setSearchText(target.value);

  return (
    <div className="App">
      <h2>Click on an item to toggle the border colour.</h2>

      <SearchBar onChange={handleSearchBarChange} value={searchText} />

      {fadeInListItems}
    </div>
  );
}

const SearchBar = (props) => {
  return (
    <>
      <label>Search Bar: </label>
      <input onChange={props.onChange} value={props.searchText} type="text" />
    </>
  );
};

const Item = (props) => {
  const [isClicked, setIsClicked] = useState(false);

  const [styles, api] = useSpring(() => ({
    border: "2px solid black",
    margin: "5px",
    borderRadius: "25px",
    boxShadow: "2px 2px black",
    backgroundColor: "white",
    color: "black"
  }));

  const handleClick = (e) => {
    api.start({
      backgroundColor: isClicked ? "white" : "red",
      color: isClicked ? "black" : "white"
    });
    setIsClicked((prev) => !prev);
  };

  return (
    <animated.div style={styles} onClick={handleClick} key={props.data.key}>
      {props.data.name}
    </animated.div>
  );
};

Solution

  • You can achieve that effect by hiding the filtered elements with max-height (along with fade). This way the items will "collapse" rather than just fade so remained elements will "slide" up.

    The transision

    const transition = useTransition(filteredItems, {
      from: { opacity: 0, marginTop: 5 },
      enter: { opacity: 1, maxHeight: 50, marginTop: 5 },
      leave: { opacity: 0, maxHeight: 0, marginTop: 0 }
    });
    

    I also added overflow: hidden to complete the effect of maxHeight and removed the margin: 5px of the Item because I added margin in the transition --definition.

    const [styles, api] = useSpring(() => ({
      border: "2px solid black",
    --  margin: "5px",
      borderRadius: "25px",
      boxShadow: "2px 2px black",
      backgroundColor: "white",
      color: "black",
    ++  overflow: "hidden",
    }));
    

    https://codesandbox.io/s/react-spring-demo-change-border-colour-on-click-forked-7fdkl