joshwcomeau / react-flip-move

Effortless animation between DOM changes (eg. list reordering) using the FLIP technique.
http://joshwcomeau.github.io/react-flip-move/examples
MIT License
4.08k stars 259 forks source link

Workarounds to prevent known issue of array change glitching? #195

Open zxol opened 6 years ago

zxol commented 6 years ago

Thanks for a great library!

In my use case, I am using FlipMove to animate a list of items up to 100 items that can be filtered down to none instantly or filtered differently, in rapid succession . As documented, this causes some weird glitches with FlipMove.

Is there anything I can do avoid this? Perhaps updating the array batchwise, with a small delay between groups? Or even delay each array update? I'm guessing that will cause a ton more render calls, would that be right?

joshwcomeau commented 6 years ago

Hey there, sorry for the delay!

Yeah, so interrupts (especially when interrupts involve items being added/removed) are a very hard problem that we haven't been able to solve yet. It's something I wish I had a better answer for, but I suspect a proper fix would involve a teardown/rebuild with WAAPI, which introduces a bunch of its own issues (like very poor browser support without a hefty polyfill).

The safest approach has some unpleasant UX side-effects: if you disable the filtering while a filter is in progress, you remove the complexity of interrupts. So if your transition length is 500ms, just disable the filter options for 500ms, grey them out in the UI.

You could try the approaches you listed (like updating in batches) but I'm not hopeful about that solution. I don't know if I understand what you mean by "delay each array update", maybe you're talking about the same thing I am, with disabling interrupts? If so, I don't believe it would cause unnecessary renders - FlipMove will render every time it receives new props.

Hope it helps! Let me know what you wound up doing :)

ggregoire commented 6 years ago

Using leaveAnimation={null} fixed it for me.

Full code:

      ...
        <tbody>
          <FlipMove
            duration={300}
            easing="ease-out"
            enterAnimation="fade"
            leaveAnimation={null}
            typeName={null}
          >
            {sortBy(items, sortPredicate).map(item => (
              <tr key={item.id} id={item.id} onClick={this.handleClick}>
                ...
mrlubos commented 6 years ago

@ggregoire How did you come up with that! I've been struggling with duplicate elements forever, this works like a charm.

shaunsaker commented 4 years ago

@ggregoire Typesafe equivalent is to use leaveAnimation='none'.

keegan-lillo commented 4 years ago

For posterity: There is another way to guard against this if you do want a leaveAnimation. If you add an onStartAll callback to check if the number of domNodes is unexpected, you can remount the <FlipMove> component by updating a key prop on it.

Example:

function Foo({ someList }) {
  const [overloadCounter, setOverloadCounter] = useState(1)
  const overloadTimoutRef = useRef(-1)
  const duration = 500

  // clean up the timer
  useEffect(() => () => clearTimeout(overloadTimoutRef.current))

  return (
    <FlipMove
      key={overloadCounter}
      duration={duration}
      onStartAll={(childElements, domNodes) => {
        const numDomNodes = domNodes.length
        clearTimeout(overloadTimoutRef.current)

        // check after the duration has elapsed if we have an unexpected number of DOM nodes
        overloadTimoutRef.current = window.setTimeout(() => {
          if (numDomNodes > someList.length) {
            setOverloadCounter(overloadCounter + 1)
          }
        }, duration)
      }}
    >
      {someList.map(({ id, content }) => (
        <div key={id}>{content}</div>
      ))}
    </FlipMove>
  )
}