Process synchronous event beats in the frame that requested them - #58108
Draft
janicduplessis wants to merge 1 commit into
Draft
Process synchronous event beats in the frame that requested them#58108janicduplessis wants to merge 1 commit into
janicduplessis wants to merge 1 commit into
Conversation
`EventEmitter::experimental_flushSync` only *requests* a beat, which is processed at the next `EventBeat::induce`. On Android the induce happens within the frame, before drawing, so a synchronous request made during layout is processed in that frame. On iOS it is not: the run loop observer that induces the beat runs before Core Animation's commit observer, so a request made from `layoutSubviews` — inside CA's commit cycle — is only processed one frame later. `AppleEventBeat` now also schedules an induce in the display phase of the current commit cycle. Core Animation runs a commit as layout → display → commit, so a zero-sized layer marked as needing display during layout has its `display` called after the whole layout pass and before the transaction is committed. A layer is kept in every visible window of every foreground scene, since the request can come from any of them — a modal and the LogBox are windows of their own — and only a layer in the tree being committed is guaranteed a display this cycle. Requests within one cycle coalesce into a single induce, so mounting ten observing views is one beat rather than ten. Two related fixes in `EventBeat` itself: a synchronous request is no longer stranded behind an already-scheduled asynchronous beat (it would silently lose its this-frame guarantee, and the leftover flag would make an unrelated later beat blocking), and `induce` becomes public so platform beats can call it from a callback. `AppleEventBeat.cpp` becomes `.mm` for the Objective-C. Covered by new unit tests in `EventBeatTest.cpp`. This is the platform half of the safe area insets work: it is what makes an inset change reported from `layoutSubviews` render in the frame it happened in. `VirtualView` uses the same mechanism.
This was referenced Aug 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary:
EventEmitter::experimental_flushSynconly requests an event beat; the beat is processed at the nextEventBeat::induce. On Android the induce happens within the frame, before drawing, so a synchronous request made during layout is processed in that same frame. On iOS it is not: the run loop observer that induces the beat runs before Core Animation's commit observer, so a request made fromlayoutSubviews— inside CA's commit cycle — is only processed one frame later.AppleEventBeatnow also schedules an induce in the display phase of the current commit cycle. Core Animation runs a commit as layout → display → commit, so a zero-sized layer marked as needing display during layout gets itsdisplaycall after the whole layout pass and before the transaction is committed. A layer is kept in every visible window of every foreground scene: the request can come from any of them (a modal and the LogBox are windows of their own), and only a layer in the tree being committed is guaranteed a display this cycle. Requests within one cycle coalesce into a single induce, so mounting ten views that each emit a synchronous event is one beat, not ten.Two fixes in
EventBeatitself:inducebecomes public so a platform beat can call it from a callback. This widens the C++ surface by one method; the alternative is a friend declaration or a lambda indirection, and neither seemed worth it for an already platform-facing class.AppleEventBeat.cppbecomes.mmfor the Objective-C.Risk: this changes when queued events are flushed on iOS for every
experimental_flushSynccaller — today that isVirtualViewand, with the rest of this stack, safe area insets. The worst case is a beat processed a frame earlier than before, inside a Core Animation commit; the run loop observer path is untouched and still catches anything the display phase misses. Revert is self-contained.This is the platform half of the safe area insets work: it is what lets an inset change reported from
layoutSubviewsrender in the frame it happened in. Split out of #57967 because it stands on its own.Changelog:
[IOS] [FIXED] - Process synchronous event beats in the frame that requested them, instead of the next one
Test Plan:
New unit tests in
EventBeatTest.cppcover the beat semantics: a synchronous request during an already-scheduled asynchronous beat, coalescing, and induce ordering.Behaviour on device was verified on the prototype this is split out of (#57967): an RNTester example renders a loud marker while a view observes the safe area but has not received an inset event yet, so any presented marker frame means the dispatch was not synchronous. With this change the marker state is committed but never presented — captures of apply/rotate/rotate decomposed frame by frame contain zero marker frames. Without it, the marker is presented for one frame on apply and rotations render with the previous orientation's insets. Videos of both are in #57967.
Regenerated the C++ API snapshots (
scripts/cxx-api/parser, Doxygen 1.16.1): the only deltas areinducemoving to public and the two newAppleEventBeatmembers.Stack — split out of #57967, which stays open as the prototype and design discussion. GitHub will not take a fork branch as a pull request base, so each of these targets
mainand its diff contains the ones below it until they merge.This is the bottom of the stack, so its diff is already just this change.
👉 1. #58108 — Process synchronous event beats in the frame that requested them
2. #58109 — Add an
experimental_onSafeAreaInsetsChangeview prop3. #58110 — Report the window safe area insets through Dimensions
4. #58111 — Warn in development when a view reports its safe area insets in a loop
5. #58112 — Render the internal SafeAreaView from the safe area insets prop
6. #58113 — Remove the native SafeAreaView and the deprecated public export