Skip to content

Instantly share code, notes, and snippets.

@aholachek
Last active March 8, 2020 17:05
Show Gist options
  • Save aholachek/32d0aef7fce6a5523dec238714e0d23b to your computer and use it in GitHub Desktop.
Save aholachek/32d0aef7fce6a5523dec238714e0d23b to your computer and use it in GitHub Desktop.

Here's a rough draft proposal for an extension to the animated component that can handle both simple and advanced FLIP use cases.

Example usage

1. Simplest example

Initiate a FLIP animation on a component by updating the flipKey prop.

Demo

Code

2. Two different components example

Match unconnected components across renders by making sure they share the same flipId prop, and when one unmounts and the other mounts, they will be connected with a FLIP animation.

Demo

Code

3. Advanced custom spring example

Occasionally, like in the example below, users will want to be able to FLIP pre-existing springs, if for instance a FLIP could be interrupted by a drag. The api allows for this as well, by passing in a flipSet prop in addition to a flipKey or flipId.

(Rough) demo

Code

Proposed extension to animated component

This is a first draft of the code wrapping the animated component to make the above three use cases possible. The AnimationHandler outer wrapper attempts to simplify the most common use cases. However, if you pass in the flipSet prop you can bypass this to get total control of the animation to facilitate interruptability and more complex effects.

Code

To do:

  1. Think about improving or simplifying flipId/flipKey/flipSet api.
  2. Figure out chained staggering of FLIP components and/or whether it is necessary (react-flip-toolkit supports it).
  3. Figure out how to lazily import FLIP functionality into animated so as not to bloat the bundle for users who don't need FLIP. (or perhaps just have a FlippableAnimated component or similar?)
  4. Make the springs customizable for abstracted FLIP cases (like examples one and two). this would be easy with a flipSpring prop or something but that might start making the api too complex.
@aleclarson
Copy link

A few of my initial thoughts on the API direction:

  • We should steer toward a hook-based API first, since that lends to better reusability/composability of animation logic.
  • We haven't needed custom props on animated components yet, and we should keep avoiding that, IMO.
  • Let's try to reuse the useSpring props API as much as possible, for familiarity and flexibility's sake.
  • We may want to provide a <FlipContext> component to let users decide where flipped components are rendered whilst flipping, which is especially important for multi-parent flips.

If we follow these constraints, I think a lot of our decisions will come naturally. For example, chaining of FLIP animations can be achieved with useChain, and a useFlip hook would be easy to tree-shake out for users that don't need it.

With all that said, here's a first draft of what a hook-based API might look like:

Flipping reordered children

import { useFlip } from 'react-spring'

const Parent = ({ items }) => {
  // The props API of "useFlip" is almost identical to "useSpring" props,
  // except no "to" or "from" props and probably no "reverse" prop. This
  // should give users unlimited control of flip animations.
  const Flip = useFlip({
    config: {frequency: 0.5},
    onStart() {},
  })
  // The "Flip" component does a few things. It tracks the order of
  // keyed children. When the order changes, it animates each child
  // from their previous position to their current position. Any children
  // that are "div" elements (instead of "animated.div") are cloned with
  // "animated.div" as their type. If we wanted, the "Flip" component
  // could even diff the "style" prop of each child on re-render, and
  // animate any changes it finds (like "backgroundColor").
  return (
    <Flip>
      {items.map(item => (
        <div
          key={item.id}
          style={{ width: 100, height: 100, color: item.color }}
        />
      ))}
    </Flip>
  )
}

Flipping between parents

Still thinking about this one. Open to suggestions!

I think the to and from props could work wonders here. For example, passing the equivalent of a flipId to useFlip would make the returned <Flip> component animate its child using the styles of the flipId element as either its from or to values, depending on which prop you defined.

// In component A:
const Flip = useFlip({
  key: 'unique flip key',
})
return <Flip>{childA}</Flip>

// In component B:
const Flip = useFlip({
  from: 'unique flip key',
})
return <Flip>{childB}</Flip>

In the example above, childB would animate from childA styles to its own styles.

Btw, we could also export a <Flip> component that has the same API as useFlip.

@dbismut
Copy link

dbismut commented Dec 23, 2019

@aholachek many thanks for this. Take my comment with a grain of salt, I may have a very naive approach on FLIP!

1. Simplest example

I'm not sure how this is related to FLIP, but I was hoping that this potential PR could animate auto. I know some people ask why this doesn't work with react-spring:

const [show, setShow] = useState(false)
const spring = useSpring({ height: !show ? 0 : 'auto' })
return <animated.div style={spring} onClick={() => setShow(!show)} />

The above makes me think that maybe we could get rid of flipKey in the first example if we delegate the animation to useSpring... Of course the upgraded animated component needs to know when to measure its bounds, but in that case getSnapshotBeforeUpdate would work here I guess?

The problems I anticipate:

  • this would measure bounds on every render, where flipKey guarantees that this would only happen when flipKey has changed, but I don't know if this would lead to a major performance issue.
  • if we use a more imperative API with const [spring, set] = useSpring(() => ({...})) then it wouldn't trigger a re-render and therefore getSnapshotBeforeUpdate wouldn't be of any use... I don't know how animated could be aware of set being called.

2. Two different components example

I'm not exactly sure how the previous example would translate API-wise, although this might work?

const [big, setBig] = useState(false)
const spring = useSpring({ height: big ? '4rem' : '2rem' })
return { show ? (
    <animated.div flipId="big" style={spring} onClick={() => setBig(!big)} />
  ) : (
    <animated.div flipId="big" style={spring} onClick={() => setBig(!big)} />
  )
}

Side note: I think styled-components is living a flipId markup on the components which shows in the HTML. I think emotion has a way to prevent non valid props to bubble down to HTML elements.

Again, this might be stupid or naive, I haven't dug too much in the internals of <FlippableAnimatedDiv /> and I would understand that this might not work for you. But I like the idea of being able to separate the animation (useSpring) from the component!

@aholachek
Copy link
Author

aholachek commented Dec 24, 2019

Thank you both for your comments!

@aleclarson, I would defer to you on using hooks. It looks like the api you're proposing in your first example would be focused on list re-ordering, though, which is only one possible use case for flip.

In your second example of animating two separate components, i'm not sure I understand what the benefits using hooks over a wrapper component would be, or why

const Flip = useFlip({
  key: 'unique flip key',
})
return <Flip>{childA}</Flip>

would necessarily be superior to

return <Flip key='unique-flip-key'>{childA}</Flip>

@dbismut one issue with using flip to animate to height:auto is that FLIP uses transforms, which won't affect the layout of the rest of the page. So you will get a very different looking animation if you use scaleY versus height. Maybe that's an ok tradeoff, but I would just want to know what sort of effects people were hoping to achieve. I guess in my head I think of animating to height:auto typically is for something like this collapse, which wouldn't work as well as a flip animation. Was that sort of what you were thinking of or did you have other types of animation in mind?

@aholachek
Copy link
Author

Actually @aleclarson, please disregard the second point. I reread your comment and I see that by using useFlip you would hope to keep the api more consistent with useSpring which makes sense.

@ShanonJackson
Copy link

ShanonJackson commented Feb 9, 2020

Built a really simple example.

Works only with context parent "Flip"
And functional component "FlipItem" which just renders an animated div and registers itself in the context.
Runs useLayoutEffect every update and if their DOMRects normalized against where they would be with no transform (this is important) then they wont animate.
Uses matrix math (scale is useless just there as a placeholder).

There might be some drag-and-drop logic floating around because i had to rip my dnd stuff out of this implementation to just show the flip stuff but my real-world usecase for flip is more for drag-and-drop.

What iv'e learnt:
Must compute position logic as if transforms aren't applied.
Can't really use the inbuilt animated.div myself without forking react-spring but ideally it would just be animated.div id="xyz"
Must use context to provide seemless multi-parent transitions.
Can use either useLayoutEffect OR getSnapshotBeforeUpdate
Doesn't animate height/width to keep it simple however this would be very easy to add

https://codesandbox.io/s/drag-and-drop-spring-i6tse

EDIT: My drag-and-drop implementation utilizing flip is almost done but haven't had time to work on it but the only thing missing is multi-container drop. Implementing flip you can build drag-and-drop without ANY position math for moving elements around.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment