question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Should React use requestAnimationFrame by default?

See original GitHub issue

Consider the following sample code: (pasted here too)

class extends React.Component {
    private canRender: boolean = false;
    private latestData: any;

    constructor(props) {
        super(props);

        let nJobs = 0;
        let lastRenderTime: number;
        props.someObservableThing.listen(data => {
            nJobs++;

            this.latestData = data;

            if (this.canRender) {
                const now = performance.now();
                this.canRender = false;
                this.setState({
                    data: this.latestData,
                    jobsPerRender: nJobs,
                    fps: (lastRenderTime === undefined) ? 0 : 1000 / (now - lastRenderTime)
                });
                nJobs = 0;
                lastRenderTime = now;
            }
        });

        this.state = {};
    }

    /* Lifecycle */
    componentDidMount() {
        this.canRender = true;
    }

    componentDidUpdate() {
        this.canRender = true;
    }

    render() {
        outputStats(this.state);
        return this.state.data === undefined ? null : <View {...this.state.data} />
    }
}

When outputStats is hit - I’m getting framerates of like 2000fps. In other words requestAnimationFrame does not seem to be a limiter for react itself.

Is this correct?

(as a slightly separate topic- if that is true, for animation things do you think it would be good to simply wrap the if (this.canRender) {} block in a requestAnimationFrame()? I guess that’s not really a React question though since the observableThing could also be capped via ticks…)

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Reactions:1
  • Comments:37 (17 by maintainers)

github_iconTop GitHub Comments

46reactions
gaearoncommented, Jan 16, 2018

Most events are intentional and require permanent effects such as two clicks happening the same event loop. You need the result of the first to happen before the second one.

Since these events can happen multiple times in a single frame, rAF batching is not really a suitable fix to that problem.

I don’t quite follow, unless you’re talking about debouncing events, which isn’t what I mean.

I don’t think that’s what Sebastian is saying. I think what he’s saying is that it’s important for the result of every click event to flush before the next click event. Consider a button that sets disabled={this.state.hasSent} and onClick={this.state.hasSent ? null : this.sendMoney}. If sendMoney sets hasSent to true, the product developer expectation is that onClick will not fire again. Otherwise two fast clicks can send the money twice, and that is a very error-prone conceptual model.

This is why we think it is important to treat setState inside interactive events (such as clicks) differently from setState inside, for example, network responses. And my understanding is that our plan is to flush “interactive” updates (like clicks) at the end of the event, but flush “other” updates (including network) with a low priority, chunked over rIC.

Andrew’s earlier point was that once we switch over to that approach, using setState for animation will not be practical by default because React will only flush it once a second or so. To fix this, the user would need to move their setState calls into a special call that flushes immediately (called flushSync). This “opts in” a setState into being processed synchronously. Since it is important when this happens we think it would be user’s responsibility to put that call inside a rAF. However we don’t exclude providing some hook to coordinate this with other libraries.

To sum up:

  1. We think delaying all flushing until a rAF breaks React rendering model and developer expectations in cases where the value of event handler depends on the state, or when event handlers read the state.
  2. We plan to keep setState inside of interactive events (such as click) acting like now, with flushing at browser event boundary.
  3. Nevertheless we plan to “downgrade” any other setState calls to low priority, meaning that if it comes from a network request or some non-interactive event, it will be processed separately, split over rIC and flushed within a second or two.
  4. We will offer a new flushSync(fn) method as an opt out of async-by-default behavior. If setState is called inside of fn it is flushed right after it exits. This is an important escape hatch for some scenarios where you really need to manually flush the update before React considers it necessary. For example if you’re in a click handler and you want to measure a DOM node after a setState and use that information for final rendering result.
  5. Making setState low-priority outside of events we decided must flush (like clicks) will “break” users who use it for animation (because their updates won’t be flushed inside events like onMouseMove—the exact scenario you’re not happy with). As a migration path we will ask them to call their setState inside flushState in a requestAnimationFrame callback. We don’t want to “own” that callback because we’re not the only ones who might want to use it, and therefore the user should be in control of it.

Does this help at all? I might have made a mistake somewhere so I’ll ask Andrew and Seb to verify, but this is my understanding.

The fifth point is not a final decision, we just haven’t gotten to it yet. We might use a strategy that’s a bit more obvious to the user or less error-prone. The intended takeaway is that: (1) I don’t see how we could use rAFs for clicks, (2) we already have a more efficient strategy in the work for non-clicks (rIC) so adding rAF in the mix doesn’t make it better, (3) we’ll need to think more about animation-specific use case, but it will likely build on top of the more flexible flushSync promitive we’ll have to expose anyway, (4) the user won’t get over-rendering animation by default as you’re worried, instead the animation will flush too rarely, leading them to research the right rAF solution (which we will document or provide a convenience API for).

13reactions
gaearoncommented, Aug 30, 2018

FWIW we’ve since stopped using requestIdleCallback because it’s not as aggressive as we need. Instead we use a polyfill that’s internally implemented on top of requestAnimationFrame. Although conceptually we still use it to do idle work.

Read more comments on GitHub >

github_iconTop Results From Across the Web

How to Use requestAnimationFrame with React | Pluralsight
This guide covers the use of the requestAnimationFrame method to create optimal animations in react. The codebase is available on github.
Read more >
Does React use requestAnimationFrame? If so, how does it ...
React doesn't currently use requestAnimationFrame to do DOM updates (as we call it, the "batching strategy"). The batching strategy is ...
Read more >
Using requestAnimationFrame in ReactJS | by Savita Patel
The requestAnimationFrame() tells the browser that you wish to perform an animation and requests that the browser calls a specified function to update...
Read more >
requestAnimationFrame and useEffect vs useLayoutEffect
While trying to implement an animated number counter I stumbled upon an interesting issue with useEffect and requestAnimationFrame not playing ...
Read more >
Stop Using setInterval. Use requestAnimationFrame
The reason for this is because you must re-call requestAnimationFrame inside your function to queue up the next time the function will be...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found