Skip to content

feat: useTransition rewrite #809

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 43 commits into from

Conversation

aleclarson
Copy link
Contributor

@aleclarson aleclarson commented Sep 9, 2019

⚠️ More testing required. See demos here

This PR is a rewrite of useTransition from scratch in hopes of simplifying both the API and the internals. Here's an example of the new API:

import {animated, useTransition} from 'react-spring'
const MyComponent = ({ items, ...props }) => {
  // A function is returned, instead of an array.
  const transition = useTransition(items, props)
  // Call the function to render your items.
  // The "values" argument is just like what "useSpring" returns.
  // The "item" argument is the item being transitioned.
  return transition((values, item) => (
    <animated.div style={values}>{item}</animated.div>
  ))
}

Once #670 is merged, you can pass a deps array and you'll get the update and stop functions returned to you:

const [transition, update, stop] = useTransition(items, props, [...deps])

Also, the Transition component has changed. Its children prop now takes different arguments:

<Transition
  items={items}
  from={{ opacity: 0 }}
  enter={{ opacity: 1 }}
  leave={{ opacity: 0 }}>
  {(values, item) => (
    <animated.div style={values}>{item}</animated.div>
  )}
</Transition>

Notable changes

  • The keys argument is now the key prop
    • Note: The key prop is optional for primitive items and mutable object items
  • Returns a transition function instead of a transitions array
    • Pass a render function to it and you'll get the animated values for each item
    • The element you return has its key prop set for you automatically
    • The phase isn't exposed to the user anymore
  • Now safe to use in concurrent mode
  • Unique mode is the new default (and the unique prop was removed for simplification)
  • Lazy mode is the new default (and the lazy prop was removed in favor of expires)
  • Event props cannot be passed directly (eg: props.onFrame), but you can still pass them in specific phases (eg: props.enter.onFrame)
  • The reset prop now uses the initial prop if it exists
  • The onDestroyed prop was removed (was there a good use case for this?)
  • The order prop was removed

New key prop

Alias: keys

When immutable objects are passed as items, they need explicit keys in order to reconnect them with any existing transitions.

For an array of immutable objects, pass a function that maps over the array and returns the unique id of each item...

key: item => item.key,

... or pass an array of keys.

// Using lodash.map
key: _.map(items, 'key'),

For a single immutable object, you can pass its key directly (no function required).

key: item.key,

New expires prop

For controlling when deleted items are dismounted after their leave animation finishes. Set to 0 for immediate dismount. By default, dismounting is postponed until (1) next render, or (2) all transitions are resting. Any value above 0 is interpreted as "milliseconds to wait before dismounting is forced".

@aleclarson
Copy link
Contributor Author

aleclarson commented Sep 9, 2019

Todo

  • Get existing demos ready to go (see here)
  • Make a codemod
  • Add a modal demo
  • Implement this

Questions

  • Currently, "expired" transitions are dismounted whenever the component re-renders, even if other controllers are still animating. Is this good or bad? 🤔
  • Currently, there's no mechanism for preventing dismounts altogether. Do any use cases need such a thing?

Ideas

  • New prop: Force items to finish entering before they can start leaving?
  • New prop: Prevent new items from entering until old items have finished leaving?
  • New prop: Force items to enter/leave one at a time?
  • New prop: Skip transitions for certain values (as identified with a user-provided function)

@aleclarson aleclarson requested review from drcmda and dbismut September 9, 2019 18:18
@aleclarson aleclarson force-pushed the feat/use-transition-rework branch 6 times, most recently from 78bc5a5 to 672393e Compare September 11, 2019 22:49
@guopengliang
Copy link
Contributor

Feature proposal for the new expires prop:

  1. pass in a single animated value, or an array of animated values, so unmount will wait for these additional animated value(s) to rest, or if they're already at rest.
  2. pass in a single Promise, or an array of Promises, so unmount will wait for the Promises to resolve.

A real-world use case for (1) above is when animating an interactive chart. Use a Spring value to animate the chart's x/y axis, while useTransition to manage the enter/leave/update of elements on the chart.

// Pass the animated value `x` to the `expires` prop of transition,
// so that the unmount event can wait for the X-axis animation to rest,
// instead of in the middle of X-axis animation.
const [{ x }, setAxis] = useSpring(() => { x: 100 });
const transition = useTransition(elements, { ...props, expires: [x] });

@aleclarson aleclarson force-pushed the feat/use-transition-rework branch 3 times, most recently from 7ff0d8d to 0d71b81 Compare September 18, 2019 16:06
aleclarson added a commit to pmndrs/react-spring-examples that referenced this pull request Sep 18, 2019
@aleclarson aleclarson mentioned this pull request Sep 18, 2019
8 tasks
@aleclarson aleclarson force-pushed the feat/use-transition-rework branch 3 times, most recently from 423592a to bef8d1c Compare September 20, 2019 21:24
@aleclarson aleclarson force-pushed the feat/use-transition-rework branch from d6baddb to 1854f36 Compare September 21, 2019 22:08
@aleclarson aleclarson force-pushed the feat/use-transition-rework branch from 1854f36 to 1a2be58 Compare September 27, 2019 16:52
@aleclarson aleclarson force-pushed the feat/use-transition-rework branch 2 times, most recently from aa2db98 to 6b3a7c0 Compare September 29, 2019 20:53
@Lynges
Copy link

Lynges commented Oct 24, 2019

@aleclarson My apologies. Don't know why I thought I was linking a new sandbox. I am indeed working from the code you linked.

I cannot prevent it from running the last line in the enter callback: cancelMap.get(item)()

If I comment out that line, then reset appears to reset the animation, but not the duration which results in the animation running faster to complete at the same point in time. At least that is what appears to be happening.

@Lynges
Copy link

Lynges commented Oct 24, 2019

@aleclarson Just now saw your edit. I tried it, but it still runs cancel just as I click the reset button.

life.pause() works, as does life.start() and life.stop(), but if I call any of update, set or reset, the promise returns as finished and cancel is called.

@aleclarson
Copy link
Contributor Author

@Lynges Yeah that's a bug I'm looking into now. My suggested edit was still wrong though, for other reasons. Give me a minute to figure this out. :)

@aleclarson
Copy link
Contributor Author

aleclarson commented Oct 24, 2019

@Lynges Ok, sorry for the wait. Things got a little tricky (and I was distracted by the React Conf stream 😜), but 6 fixes later, everything should work as expected.

Add the following prop to the next({ life: '0%' }) animation:

onRest: ({ finished }) => finished && cancelMap.get(item)()

Note: You'll need to install 9.0.0-canary.808.9.7e75a67

PS: You can become my patron to support my work.

@Lynges
Copy link

Lynges commented Oct 25, 2019

Thanks a bunch @aleclarson :) Got it all working. Found a couple of things with regards to when the components are re-rendered. I will do some more digging on Monday to see if it's just my code or something that might be relevant.
Again, thanks a lot for your help and quick response :)

@dellwatson
Copy link

dellwatson commented Dec 10, 2019

I still don't understand how to toggle with new useTransition

const [toggle, set] = useState(false)
const transitions = useTransition(toggle, null, {
from: { position: 'absolute', opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
})

return transitions.map(({ item, key, props }) => 
item
  ? <animated.div style={props}>😄</animated.div>
  : <animated.div style={props}>🤪</animated.div>
)

@dimroth
Copy link

dimroth commented Dec 10, 2019

@dellwatson Hi, the following should work if you pass the proper index but there might be better options :

const items = ['😄',' 🤪']
export default () => {
  const [toggled, setToggled] = useState(false)
  const transition = useTransition(items[+toggled], {
    from: { opacity: 0 },
    enter: { opacity: 1 },
  })

  return (
    <>
      <button onClick={() => setToggled(!toggled)}>Switch please</button>
      <div className="simple-trans-main">
        {transition((values, item) => (
          <animated.div style={values}>{item}</animated.div>
        ))}
      </div>
    </>
  )
}

Live example here : https://codesandbox.io/s/eager-cerf-r78kw

@Inviz
Copy link

Inviz commented Mar 23, 2020

Does immediate: true do anything in this version? Can't get it to work, it feels like it's just ignored.

@aleclarson
Copy link
Contributor Author

@Inviz Which version are you using?

@rayandrew
Copy link

Hi @aleclarson, I am trying the latest canary.

My question : is there any effect if I set the reverse option in useTransition?

I want to reverse the transition animation from last item to first item instead of first item to last item like usual and it seems that option is not working at all

Thank you in advance!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants