Skip to content
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

Animating when adding/removing a single component #226

Closed
sophiebits opened this issue Jul 23, 2013 · 17 comments
Closed

Animating when adding/removing a single component #226

sophiebits opened this issue Jul 23, 2013 · 17 comments

Comments

@sophiebits
Copy link
Collaborator

It's useful to be able to animate the appearance and disappearance of a particular component.

It's possible to animate entrances without too much trouble by hand, using componentDidMount but exits are harder since elements disappear immediately after componentWillUnmount and don't give the animation time to finish.

What can we add to React to make this easy?

(See also #227.)

@jordwalke
Copy link
Contributor

Here's a thought: Right now, the when any props of a div/span have changed, the reconciler creates a "plan" for how to carry out an update. This plan consists of:

  • New markup m that must be inserted at location m'.
  • New items n that must be inserted at location i'.
  • Existing items at location d that must be deleted.

Right now, the default behavior is to simply carry out this plan in the most efficient way that we can. What if, you could tell these native components that you want to handle the insertions/removals/moves in a very particular way (that may include animation).

Something like:

 <div childManager={TheChildManager}>
     {childrenThatEvolveOverTime}
 </div>

Where TheChildManager might look like:

var ChildManager = {
  addChildren: function(childrenMarkup, location) {

  },
  moveChildren: ...
  removeChildren: ...
};

What's interesting is that not only are child instances added/moved/removed, but also updated. We need an API that allows controlling when/how these updates occur throughout the process of creating/destroying/moving. You may wish to update before you move existing children/add new ones, or you may wish to update them after all the adding/movement is complete.

It seems like the best strategy is to make sure that the lowest level hooks are exposed, so that no one ever feels completely trapped by not being able to perform their particular animation feature. Then, on top of that, we can build sugar that does some very common things, such as:

<div childManager={require('ReactAnimateNewRows')} />

@chenglou
Copy link
Contributor

I'm also really hyped up for animation, here's the old issue: #161
But @spicyj, concerning #227, I don't agree with adding enterAnimation and exitAnimation. This special-cases those two situations a bit too much, and lots of unique dynamic situations in-between would need formal attributes too, which really limits the framework and the coder's creativity.

I like @jordwalke's idea. This also allows for graceful degradation. And the best part is it's totally unobstructive and could be used as a drop-in mixin. I'm kind of drooling at the possibilities lol.

One thing I'm worried about is the parent/siblings' expectation of what should/should not happen during/after the animation, but I'm sure it can be worked out.

@sophiebits
Copy link
Collaborator Author

Oops, I forgot about your earlier issue! Agree that more flexibility is always better, was just brainstorming some possible new APIs – we want simple things to be easy.

@jordwalke
Copy link
Contributor

A couple of other features that I've heard requests for (I think this hits on @chenglou's comment):

- Being able to block various forms of updates while mid-animation.
-- Being able to block external props updates while you are animating your component.
-- Being able to block all non-animation related updates while animating. Effectively, we would queue up those updates until the animation is complete (sounds like the batching diff that @spicyj built will help here).

Another completely different direction to take, which doesn't really address animating child components but focuses more on property values:

Pete Hunt's work on animatable state transitions:

  this.tweenState({
      from: {stateFieldOne: this.props.starterOne, stateFieldTwo: this.props.starterTwo},
      to: {stateFieldOne: this.state.starterOne*2,  stateFieldTwo: this.state.starterTwo*2},
      timingFunc: 'ease-in'        
  });

Which would just call setState several times, throttling to hit the 16ms animation frame window.

We've also talked about being able to implement tweenState without actually having to call setState every 16ms - we could use the from and to as "snapshot" points, that we can perform a single reconciliation diff between, calculate the diff and tween those individual values every 16ms. It may not be possible to compute two parallel tweenStates using this method.

@zpao
Copy link
Member

zpao commented Oct 28, 2013

I think React.addons.TransitionGroup mostly covers this. If not, let's figure out what else needs to happen to make animations in React as good as possible.

@zpao zpao closed this as completed Oct 28, 2013
@zpao
Copy link
Member

zpao commented Oct 28, 2013

Oh I guess TransitionGroup handles #227 but not this without restructuring a bit...

@zpao zpao reopened this Oct 28, 2013
@sophiebits
Copy link
Collaborator Author

Actually I think this is pretty reasonable with TransitionGroup.

@sophiebits
Copy link
Collaborator Author

In the future when we have x:frag it'll be Even Better™.

@jessepollak
Copy link

Is this actually possible? I've been messing around with it, but can only seem to get it to work with an array of children.

@jessepollak
Copy link

Never mind, I was wrong! For future problem havers, you need to remove the elements inside the CSSTransitionGroup, not the CSSTransitionGroup itself :)

@michaelahlers
Copy link

Was there ever a direct resolution to this ticket (it's possible I've missed a few things in the comments here)?

To recap, it's clear how you can elegantly cause enter animations by setting state from known properties in componentDidMount, but I still don't see how the opposite effect can be accomplished, as illustrated in the following (non-functional) example:

React.createClass({
  getDefaultProps: function() {
    return { items: [1, 2, 3] }
  },

  getInitialState: function() {
    return { items : [] }
  },

  componentWillMount: function() {
    this.setState({ items: this.props.items })
  },

  render: function() {
    return (
      <ReactCSSTransitionGroup transitionName="item">
        {this.state.items.map(function(item, index) {
          return <span key={index}>{item}</span>
        })
      </ReactCSSTransitionGroup>
    )
  },

  componentWillUnmount: function() {
    // Obviously broken.
    this.setState({ items: [] })
  }
})

It's been a frustrating exercise trying to accomplish this effect in a more complex application (in my case, one using a router library and Flux pattern) where there's little or not control at components one level up the hierarchy from those wishing to do animations. If it was possible to capture and then perform leave animations as illustrated here, I'd be able to cut a huge amount of complexity.

PS, please forgive the lack of semicolons—I write a lot of Scala these days.

@chenglou
Copy link
Contributor

@michaelahlers I have a working wrapper (animation library-agnostic, mostly) here. It's a bit messy right now but once I find some more free time I'll pull it out. See chenglou/react-tween-state#15 as well.

(This sure brings back memories =))

@michaelahlers
Copy link

@chenglou, leave it to me to dig up nostalgia fuel for you. 😏 Reading your samples there is kind of funny; I was contemplating—even as I wrote my comment—about how enter and exit transitions are inadequate for a lot of animation use cases, and struggling to make them work in this framework might result in isolated, short-term gains for big-picture, long-term losses. Your work definitely looks several steps ahead! State Stream looks incredibly powerful and—with a functional programming background—equally intuitive. Combined with Tween State, I can imagine a lot of sophistication that can't be accomplished purely in CSS. That said, it looks like @irvinebroque and I are in the same boat: needing a good baby step that adds exit animation classes with node removal only after CSS animations complete.

@chenglou
Copy link
Contributor

Eh sorry, state-stream is unrelated. The wrapper should mostly work in conjunction with anything else, including a simple className change and node removal.

@michaelahlers
Copy link

You may have lost me a bit. Container, from App4, appears to have a similar usage pattern to TransitionGroup, in that it's going to apply certain CSS properties to its children based on the state of its parent—whose state is managed internally. This is where my needs fundamentally differ (I think). Putting it loosely, in my particular case, a router library is responsible for building the component tree, and I've got little control over how and when any individual component will be mounted or unmounted. That's why I'm seeking a means of triggering any arbitrary animation from componentWillUnmount (in a way that allows the transition group the keep the nodes in the DOM until animation's done) because it's fully-encapsulated. For now, it may be that I need to move more state into my Flux stores that's cleared out (which then effectively applies the componentDidMount approach to leave animations) on certain routing events.

@maxviewup
Copy link

A good way to animate a removal is delaying it. for instance, when you call the delete function, it updates the state, saying the element x will be removed. then, it sets a timeout or make a AJAX request. Meanwhile, the component will recive the signal and will animate itself.

Another way is never remove the element. it will be always there, but the visible state will be changed. Material-UI uses this, for instance.

@jtomaszewski
Copy link

This is how we handled it:

  • use Transition#props.appear to start show animation only after component is mounted
  • when Transition#props.onExited is called, call MyComponent#props.onHideTransitionEnd, to inform the parent that the hide transition has ended (so the parent knows that this component can be unmounted now)

Then, for example, if you want to:

  • mount a MyModal immediately when somebody clicks on a button
  • unmount the MyModal only after MyModal is scheduled to close and its' close animation has finished

, then you can do it like that:

const MyOpeningComponent = ({ initialModalOpen = false }) => {
  const [mountModal, setMountModal] = useState(initialModalOpen);
  const [openModal, setOpenModal] = useState(initialModalOpen);

  const handleOpen = useCallback(() => {
    setMountOverlay(true);
    setOpenOverlay(true);
  }, []);

  const handleClose = useCallback(() => {
    setOpenOverlay(false);
  }, []);

  const handleCloseTransitionEnd = useCallback(() => {
    if (!openModal) {
      setMountModal(false);
    }
  }, [openModal]);

  return (
    <>
      <button onClick={handleOpen}>Open modal</button>
      {(mountModal || openModal) && (
        <MyModal
          open={openModal}
          onClose={handleClose}
          onCloseTransitionEnd={handleCloseTransitionEnd}
        />
      )}
    </>
  );
};

bvaughn pushed a commit to bvaughn/react that referenced this issue Aug 13, 2019
Improve performance by mutating the children array
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

No branches or pull requests

8 participants