Search code examples
reactjsreact-hooksreact-spring

How can we animate the display of records using react-spring


How can we animate the display of records using react-spring. Can we call the useTransition in useEffect() ? I would like to animate records from bottom to top one by one when user navigate to the page. I have tried below nothing is happening..? Any advise would be really helpful.

https://codesandbox.io/s/inspiring-snowflake-34dpx?file=/src/App.js:711-881

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

import "./styles.css";

const array1 = [
  { id: "1", name: "Cat" },
  { id: "2", name: "Dog" },
  { id: "3", name: "Cow" }
];
export default function App() {
  const [list, setList] = useState(array1);
  const height = 20;
  useEffect(() => {
    setList(array1);
  }, []);

  const transitions = useTransition(
    Object.entries(list).map((data, i) => ({ ...data, y: i * height })),
    {
      from: { position: "absolute", opacity: 0 },
      enter: { height: 0, opacity: 1 },
      leave: { opacity: 0 },
      config: { tension: 220, friction: 120 }
    }
  );

  return (
    <div className="App">
      {/* {Object.entries(list).map((item, index) => (
        <div key={item.id} className="record">
          <span key={item.name}>{item.name}</span>
        </div>
      ))} */}

      {Object.entries(transitions).map(({ item, props, key }) => (
        <animated.div key={key} style={{ ...props, position: "absolute" }}>
          <div key={item.id} className="record">
            <span key={item.name}>{item.name}</span>
          </div>
        </animated.div>
      ))}
    </div>
  );
}

Solution

  • The transitions variable is a function, so you don't want Object.entries(transitions).map(). It's not an error because functions in Javascript are also objects, but it's not going to do anything because the array of entries is an empty array.

    You want to call the transitions function, which is its own sort of mapper.

    {transitions((props, item, key) => (
    

    Well now I'm seeing a cyan box instead of a white screen. So that's a little progress, but there's more to do.

    If you console.log your item object you'll see that it doesn't look right.

    Object.entries(list).map((data, i) => ({ ...data, y: i * height }))
    

    You are mapping an array of object entries, so the data variable is an array containing the key and the value. You don't want that. list is already an array, so ditch the Object.entries.

    list.map((data, i) => ({ ...data, y: i * height })),
    

    Now I see "Cow"! But the other two are hidden because your items are appearing on top of each other.

    You are using absolute positioning but you haven't passed the position down to the component. Since you aren't animating that value, it's a property of item rather than props.

    <animated.div key={key} style={{ ...props, position: "absolute", top: item.y }}>
    

    Now I see all three!

    But the overlapping background blocks are probably not what you intended. I'm not sure what you intended, really, because you have a height of 20 in your JS and 100px in your CSS. You also have a height property in your animation but it's not doing anything.

    All of your list items have height: 0px because you are setting the height to 0 in the enter property, which is called when an item first enters the list. On the other hand, the from property sets the initial values that the item animates away from, so maybe you want to have an initial value of height: 0 in from and some final value of height in enter.

    You can animate the height of the background, but that won't impact the height of the text. You might want a transform like scaleY. You can use a transform instead of the height animation (all items start out in their final position and grow in place) or alongside the height animation (all items start together at the top and grow down).


    Here's one possible effect: Code Sandbox Link

    style.css

    .App {
      font-family: sans-serif;
      text-align: center;
    }
    .record {
      width: 300px;
      background-color: aqua;
      margin: 20px 0;
      color: #1f1d1d;
    }
    .title {
      font-size: 30px;
      padding: 35px;
    }
    

    App.js

    import { useState } from "react";
    import { useTransition, animated } from "react-spring";
    
    import "./styles.css";
    
    const array1 = [
      { id: "1", name: "Cat" },
      { id: "2", name: "Dog" },
      { id: "3", name: "Cow" }
    ];
    
    export default function App() {
      const [list, setList] = useState(array1);
    
      const transitions = useTransition(list, {
        from: { opacity: 0, scaleY: 0 },
        enter: { opacity: 1, scaleY: 1 },
        leave: { opacity: 0 },
        config: { tension: 220, friction: 120 }
      });
    
      return (
        <div className="App">
          {transitions((props, item, key) => {
            console.log(props, item, key);
            return (
              <animated.div key={key} style={{ ...props }} className="record">
                <div className="title">{item.name}</div>
              </animated.div>
            );
          })}
        </div>
      );
    }
    

    Edit

    As requested, from the bottom to the top one after another:

    export default function App() {
      const height = window.innerHeight;
    
      const [list, setList] = useState(array1);
    
      const transitions = useTransition(list, {
        from: { opacity: 0, translateY: height },
        enter: { opacity: 1, translateY: 0 },
        leave: { opacity: 0 },
        config: { tension: 220, friction: 120 },
        trail: 200
      });
    
      return (
        <div className="App">
          {transitions((props, item, key) => {
            console.log(props, item, key);
            return (
              <animated.div key={key} style={{ ...props }} className="record">
                <div className="title">{item.name}</div>
              </animated.div>
            );
          })}
        </div>
      );
    }