What's a react.js-friendly way to animate a list-reordering?

JavascriptJqueryCssReactjs

Javascript Problem Overview


I have a list of items with scores, ordered by scores, rendered by react.js as a vertically-oriented list of rectangular items (highest scores at top). Hovers and other clicks on the individual items may show/hide extra info, changing their vertical height.

New information arrives that changes the scores, slightly, making some items rank higher and others lower after a re-sort. I'd like the items to simultaneously animate to their new positions, rather than appear in their new locations instantly.

Is there a recommended way to do this in React.js, perhaps with a popular add-on?

(In a similar past situation using D3, a technique I've used was roughly:

  1. Display the list with item DOM nodes in their natural order, with relative positioning. With relative positioning, other small changes – CSS or JS-triggered – to individual items' extent shift others as expected.
  2. Mutate all the DOM nodes, in a single step, to have their actual relative-coordinates as new absolute coordinates – a DOM change that causes no visual change.
  3. Re-order the item DOM nodes, within their parent, to the new sort order – another DOM change that causes no visual change.
  4. Animate all nodes' top-offsets to their new calculated values, based on the heights of all preceding items in the new ordering. This is the only visually-active step.
  5. Mutate all item DOM nodes back to non-offset relative-positioning. Again, this causes no visual change, but the now-relatively-positioned DOM nodes, in the natural ordering of the underlying list, will handle internal hover/expand/etc style changes with proper shifting.

Now I'm hoping for a similar effect in a React-ish way...)

Javascript Solutions


Solution 1 - Javascript

I just released a module to tackle exactly this problem

https://github.com/joshwcomeau/react-flip-move

It does a few things differently than Magic Move / Shuffle:

  1. It uses the FLIP technique for hardware-accelerated 60FPS+ animations
  2. It offers options to "humanize" the shuffle by incrementally offsetting delay or duration
  3. It handles interruptions gracefully, no weird glitch effects
  4. Bunch of other neat stuff like start/finish callbacks

Check out the demos:

http://joshwcomeau.github.io/react-flip-move/examples/#/shuffle

Solution 2 - Javascript

I realise this is a bit of an old question and you've probably found a solution by now, but for anyone else coming across this question, check out the MagicMove library by Ryan Florence. It's a React library to handle the exact scenario you describe: https://github.com/ryanflorence/react-magic-move

See it in action: https://www.youtube.com/watch?v=z5e7kWSHWTg#t=424

Edit: This is broken in React 0.14. See React Shuffle as an alternative, as suggested by Tim Arney below.

Solution 3 - Javascript

React Shuffle is solid and up to date. It's inspired by Ryan Florences Magic Move demo

https://github.com/FormidableLabs/react-shuffle

Solution 4 - Javascript

I didn't want to use a third-part library for my animations so I implemented the "FLIP" animation technique using React lifecycle methods getSnapshotBeforeUpdate() and componentDidUpdate().

When the list item's props.index changes the old position is captured in getSnapshotBeforeUpdate() and in componentDidUpdate() the css transform: translateY() is applied so that the item appears to be animating from old position to current position.

class TreeListItem extends Component {

  constructor(props) {
    super(props);
    this.liRef = React.createRef();
  }

  getSnapshotBeforeUpdate(prevProps) {
    if (prevProps.index !== this.props.index) {
      // list index is changing, prepare to animate
      if (this.liRef && this.liRef.current) {
        return this.liRef.current.getBoundingClientRect().top;
      }
    }
    return null;
  }


  componentDidUpdate(prevProps, prevState, snapshot) {
    if (prevProps.index !== this.props.index && snapshot) {
      if (this.liRef && this.liRef.current) {
        let el = this.liRef.current;
        let newOffset = el.getBoundingClientRect().top;
        let invert = parseInt(snapshot - newOffset);
        el.classList.remove('animate-on-transforms');
        el.style.transform = `translateY(${invert}px)`;
        // Wait for the next frame so we
        // know all the style changes have
        // taken hold.
        requestAnimationFrame(function () {
          // Switch on animations.
          el.classList.add('animate-on-transforms');
          // GO GO GOOOOOO!
          el.style.transform = '';
        });
      }
    }
  }

  render() {
    return <li ref={this.liRef}>
    </li >;
  }
}

class TreeList extends Component {

  render() {
    let treeItems = [];
    if (this.props.dataSet instanceof Array) {
      this.props.dataSet.forEach((data, i) => {
        treeItems.push(<TreeListItem index={i} key={data.value} />)
      });
    }

    return <ul>
      {treeItems}
      </ul>;
  }
}

.animate-on-transforms {
  /* animate list item reorder */
  transition: transform 300ms ease;
}

Solution 5 - Javascript

The best way I can think of to accomplish this off the top of my head would be to first make sure that you give every one of your elements a key as described here:

http://facebook.github.io/react/docs/multiple-components.html#dynamic-children

Next I would define CSS3 animation similar to this:

Animating lists with CSS3

Basically in your render function you would calculate where the element is suppose to go, ReactJS will place the element where it's suppose to be (make sure you give each element the same key every time! This is important to make sure ReactJS reuses your DOM element properly). In theory at least, the CSS should take care of the rest of the animation for you outside of reactjs/javascript.

Disclamer... I've never actually tried this ;)

Solution 6 - Javascript

Late to the party here, but we just released AutoAnimate which allows you to add animations for elements entering, leaving, or moving within a DOM element with a single line of code. Works with React, Vue, or any other Javascript framework and is less than 2kb in size.

Open-source and MIT licensed, hope it's helpful!

Solution 7 - Javascript

I just found this library: https://www.npmjs.com/package/react-animated-list, Its very amazing, You just have to pass the children:

import { AnimatedList } from 'react-animated-list';
import { MyOtherComponent } from './MyOtherComponent';

const MyComponent = ({myData}) => (
  <AnimatedList animation={"grow"}>
    {otherComponents.map((c) => (
      <MyOtherComponent key={c.id} />
    ))}
  </AnimatedList>
)

Demo: https://codesandbox.io/s/nifty-platform-dj1iz?fontsize=14&hidenavigation=1&theme=dark

Boom it will work for you.

Solution 8 - Javascript

Because the position of an element depends heavily on the DOM engine, I find it really hard to implement tweening and animating in a pure way within React components. If you want to do it cleanly, I think you need at least: a Store to hold the current position of an element, a Store to hold the next position of an element (or combine it with the previous store), and a render() method that applies the offset margins per element until in the final frame, the next positions become current. How you calculate the next positions in the first place is also a challenge. But given that the elements only swap positions, you could use the Store with the current positions to swap element #0 with element #1, by swapping their offsets in the next positions Store.

In the end it's easier to just use an external library to manipulate the elements' margins after they're rendered.

Solution 9 - Javascript

Just use ReactCSSTransitionGroup. It's built into the React with Addons package and is perfect for this use case! Just make sure that the component on which you apply the transition has already mounted to the DOM. Use the CSS that is in the linked example for a nice fading transition.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestiongojomoView Question on Stackoverflow
Solution 1 - JavascriptJoshua ComeauView Answer on Stackoverflow
Solution 2 - JavascriptAndruView Answer on Stackoverflow
Solution 3 - JavascriptTim ArneyView Answer on Stackoverflow
Solution 4 - JavascriptBoogView Answer on Stackoverflow
Solution 5 - JavascriptChrisView Answer on Stackoverflow
Solution 6 - JavascriptAndrew BoydView Answer on Stackoverflow
Solution 7 - JavascriptIlyas karimView Answer on Stackoverflow
Solution 8 - Javascriptuser217782View Answer on Stackoverflow
Solution 9 - JavascriptJessie SchallesView Answer on Stackoverflow