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

componentDidUpdate fires before browser paint operation #2659

Closed
jackwanders opened this issue Dec 4, 2014 · 6 comments
Closed

componentDidUpdate fires before browser paint operation #2659

jackwanders opened this issue Dec 4, 2014 · 6 comments

Comments

@jackwanders
Copy link

I'm not sure that this isn't by design, but I discovered today that the lifecycle methods executed after a React element renders (i.e. componentDidMount and componentDidUpdate, executed at the point noted below) appear to be executed before the browser's paint operation.

src/browser/ReactReconcileTransaction.js#L81

This introduced some unexpected behavior in a scenario similar to this example:

var Widget = React.createClass({
  componentWillUpdate: function() {
    this.stashedStyleValue = this.getDOMNode().style.someValue;
    this.getDOMNode().style.someValue = 'none';
  },
  componentDidUpdate: function() {
    this.getDOMNode().style.someValue = this.stashedStyleValue;
  },
  render: function() {
    return (
      <div>...</div>
    );
  }
});

Given the React class above, I would expect the following order of operations when the component re-renders in the browser:

  1. componentWillUpdate fires
  2. render fires
  3. browser re-paints the DOM
  4. componentDidUpdate fires

However, this is not the case. The actual behavior is that steps 3 and 4 occur in the reverse order. In Chrome, when I record a timeline of events, I see the following:

  1. Recalculate Style
  2. Update Layer Tree
  3. Paint x 3
  4. Composite Layers

It this example, it looks like this.getDOMNode().style.someValue changes, then changes back before the paint ever occurs, rendering my efforts futile without manually deferring the logic in componentDidUpdate using window.setTimeout.

To test my theory that the order would be different if componentDidUpdate fired after the paint operation, I made the following changes to the React source:

https://github.com/jackwanders/react/compare/defer-post-render-lifecycle-methods?expand=1

When I load the same element in the browser using the modified code, the timeline in Chrome is as follows:

  1. Recalculate Style
  2. Update Layer Tree
  3. Paint x 3
  4. Composite Layers
  5. Recalculate Style

This is the order of operations I would expect with the provided lifecycle methods defined in my example class above.

As I said, I can't say for certain that this is something that should be corrected, but I wanted to submit this issue and hopefully get some feedback from a React contributor, who is sure to have more domain knowledge and can explain why the existing flow exists, if indeed it is as designed.

Thanks.

@jimfb
Copy link
Contributor

jimfb commented Dec 7, 2014

cc: @sebmarkbage

@jackwanders
Copy link
Author

For the record, one thing I quickly noticed when deferring post-render life cycle methods is that you can no longer execute code after React.render and expect that a component's componentDidMount method had run.

...I may have noticed this when I ran the test suite and pretty much every test that asserts a list of executed life cycle methods failed. This doesn't seem to be a possible bug so much as a philosophical decision on the expectations surrounding React's render methods and their corresponding life cycle methods.

@sebmarkbage
Copy link
Collaborator

This is indeed intentional. React reconciliation life-cycle occurs within a single event loop. Because there's no clear scheduling API within an event loop, we rely on explicit batching which ensures that only one reconciliation happens within a batch. In those cases, React.render is not synchronous. Ideally it would always be asynchronous.

The use case for componentDidUpdate is to do work after React has reconciled all it's properties but you may want to do some extra work before the next frame is painted. Otherwise you would always see a flash.

Normally, all the writes you do on a DOM component has no effect until then next event loop anyway. Unless you read in between, but you have no reads in your example.

You can explicitly schedule some work to happen after the next frame is painted, using requestAnimationFrame.

Can you elaborate more on your particular use case?

@jackwanders
Copy link
Author

I totally agree with the reasoning you've laid out @sebmarkbage. I can give you some color on my particular scenario, which I also admit is something of an edge case.

I am building a UI with a particular element that can have one of three CSS position values depending on the current state of the UI: 'static', 'absolute', or 'fixed'. This element can also independently be animated along the X axis, so it will have a left value as well.

When the state of my UI dictates that this element will toggle from fixed to absolute (or vice versa), I need to execute that class transition without the element moving within the browser, meaning the left value will change at the same time the position value does. However, because there is a transition rule that targets left, this will result in the element animating when we don't want it to.

In order to get around this, the current solution is to:

  • in componentWillUpdate, set el.style.transition to none and store the old transition value for retrieval later. With no transition, position and left can both update without the element moving in the browser
  • in componentDidUpdate, set el.style.transition to the stashed value. This turns the transition back on so it can again animate along the X axis when necessary.

As I found, because componentDidUpdate fires in the same event loop as render, both style.transition changes occur before a single paint occurs, and the desired effect is not achieved. If the contents of componentDidUpdate are deferred, then we get what we're after.

Thanks for replying, @sebmarkbage, and as far as I'm concerned, this issue can be closed. If you agree, I'll leave it to you to close it.

@zpao zpao closed this as completed Dec 10, 2014
@jDeppen
Copy link

jDeppen commented Mar 18, 2015

@jackwanders would you mind sharing how you deferred the contents of componentDidUpdate? Did you use requestAnimationFrame?
Thanks!

@dustingetz
Copy link

dom reflow will happen before didUpdate (so I can measure heights), and repaint happens after didUpdate (so i can effect based on the height without flicker)?

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

6 participants