Search code examples
javascriptreactjsreact-spring

How to get transitions to work each time an element is clicked


I am trying using to use 'useTransition' and 'animated' from the React-Spring Library and having an issue where it only animates the first time, and also doesn't activate leave.

From digging into it, I think it's to do with the fact that the key isn't updating, and I think this is what enables a new animate to render, but I have no idea how to get the key to update other than to refresh the page.

function App() {
  const [showApplicants, setShowApplicants] = useState(false);
  const [showSpecificApplicant, setShowSpecificApplicant] = useState([]);
  const [applicants, setApplicants] = useState([
    {
      firstName: "Billy",
      background: "Employed",
      id: 1
    },
    {
      firstName: "Sarah",
      background: "Employed",
      id: 2
    },
    {
      firstName: "Vera",
      background: "Student",
      id: 3
    },
    {
      firstName: "Albert",
      background: "Unemployed",
      id: 4
    }
  ]);

  const transitions = useTransition(applicants, item => item.id, {
    from: {
      transform: "translate(-100%, 0)"
    },
    enter: {
      transform: "translate(0%,0)"
    },
    leave: {
      transform: "translate(150%, 0)"
    },
    config: { duration: 3000 }
  });

  const handleApplicants = () => {
    setShowApplicants(!showApplicants);
    setShowSpecificApplicant([]);
  };

  const showDetails = (e, item) => {
    setShowSpecificApplicant(item.id);
  };

  const SpecificApplicant = () => {
    const matchedUser = applicants.find(
      user => user.id === showSpecificApplicant
    );
    return transitions.map(({ item, key, props }) => {
      return showSpecificApplicant === item.id ? (
        <animated.div key={key} style={props}>
          <div
            style={{
              background: "lightblue",
              width: "20%",
              margin: "0 auto",
              textAlign: "center",
              borderRadius: "5px",
              padding: "10px",
              display: "flex",
              flexDirection: "column"
            }}
          >
            <h3 style={{ margin: "0", padding: "0" }}>
              {matchedUser.firstName}
            </h3>
            <p> Current Status: {matchedUser.background}</p>
          </div>
        </animated.div>
      ) : null;
    });
  };

  return (
    <div className="App">
      <h1>Hello</h1>
      <button onClick={handleApplicants}> Show Applicants </button>
      <ul>
        {showApplicants &&
          applicants.map(item => (
            <button onClick={e => showDetails(e, item)}>
              {item.firstName}
            </button>
          ))}
      </ul>
      <SpecificApplicant />
    </div>
  );
}

I'm expecting an animation to be triggered each time I click an applicant, but it only animates the first time. After that they just appear normally. Can anyone show me what I'm doing wrong.


Solution

  • The transition must handle the change of the array. So change applicants array to showSpecificApplicant array in the useTransition. This way it will apply the enter animation for the new array elements and the leave animation for the lement you deleted from the array.

    import React, { useState } from 'react';
    import ReactDOM from 'react-dom';
    import { useTransition, animated } from 'react-spring';
    import './styles.css';
    
    function App() {
      const [showApplicants, setShowApplicants] = useState(false);
      const [showSpecificApplicant, setShowSpecificApplicant] = useState([]);
      const [applicants, setApplicants] = useState([
        {
          firstName: 'Billy',
          background: 'Employed',
          id: 1
        },
        {
          firstName: 'Sarah',
          background: 'Employed',
          id: 2
        },
        {
          firstName: 'Vera',
          background: 'Student',
          id: 3
        },
        {
          firstName: 'Albert',
          background: 'Unemployed',
          id: 4
        }
      ]);
    
      const transitions = useTransition(showSpecificApplicant, item => item.id, {
        from: {
          transform: 'translate(-100%, 0)'
        },
        enter: {
          transform: 'translate(0%,0)'
        },
        leave: {
          transform: 'translate(150%, 0)'
        }
      });
    
      const handleApplicants = () => {
        setShowApplicants(!showApplicants);
        setShowSpecificApplicant([]);
      };
    
      const showDetails = (e, item) => {
        setShowSpecificApplicant([item]);
      };
    
      const SpecificApplicant = () => {
        return transitions.map(({ item: matchedUser, key, props }) => {
          return (
            <animated.div key={key} style={props}>
              <div
                style={{
                  background: 'lightblue',
                  width: '20%',
                  margin: '0 auto',
                  textAlign: 'center',
                  borderRadius: '5px',
                  padding: '10px',
                  display: 'flex',
                  flexDirection: 'column'
                }}
              >
                <h3 style={{ margin: '0', padding: '0' }}>
                  {matchedUser.firstName}
                </h3>
                <p> Current Status: {matchedUser.background}</p>
              </div>
            </animated.div>
          );
        });
      };
    
      return (
        <div className="App">
          <h1>Hello</h1>
          <button onClick={handleApplicants}> Show Applicants </button>
          <ul>
            {showApplicants &&
              applicants.map(item => (
                <button onClick={e => showDetails(e, item)}>
                  {item.firstName}
                </button>
              ))}
          </ul>
          <SpecificApplicant />
        </div>
      );
    }
    
    const rootElement = document.getElementById('root');
    ReactDOM.render(<App />, rootElement);
    

    You can see the result here: https://codesandbox.io/s/jolly-glade-mbzl7

    Now you may want to change the style of the applicants to be in the same row during transitions. If you add position: 'absolute' to the from object then you are on the right track.