Skip to content

Do not retain a flex basis measured during a previous layout - #2020

Open
mozzius wants to merge 1 commit into
react:mainfrom
mozzius:fix-stale-measured-flex-basis
Open

mozzius wants to merge 1 commit into
react:mainfrom
mozzius:fix-stale-measured-flex-basis

Conversation

@mozzius

@mozzius mozzius commented Sep 2, 2026

Copy link
Copy Markdown

Summary

Fixes #2019.

layout.computedFlexBasis persists across YGNodeCalculateLayout calls and is only cleared by markDirtyAndPropagate on that exact node. The resolved-flex-basis branch of computeFlexBasisForChild keeps whatever value is already stored, which is what lets a max-content measurement made earlier in the same layout survive into the definite-size pass (and is why flex: 1 inside an auto-height column means "content height"). The guard only checks whether a value exists, not which layout produced it.

When a later layout at a different root size hits an ancestor's measurement cache during the max-content pass, that subtree is skipped, the child's computedFlexBasis is never refreshed, and the definite pass retains a size measured against the previous available size. In React Native this shows up as a flex: 1 Text that keeps its landscape height after rotating back to portrait (react/react-native#58294).

Fix

Record whether a stored computedFlexBasis came from measuring the child's content (computedFlexBasisIsMeasured). In the resolved branch, a stored value that is measured and stamped with a previous generation is treated as stale: the child takes the measure path again, which does exactly what the skipped max-content pass would have done and normally hits the child's own measurement cache. A retained resolved basis is unchanged, so "definite from the start" containers still resolve flex: 1 to a basis of 0 as before, and nothing changes within a single layout.

Clearing the value instead (or enabling WebFlexBasis) is not a fix: the resolved branch then writes 0 with no measurement to follow and the child collapses to 0.

Test plan

  • New test in tests/YGRelayoutTest.cpp: an 8-node tree (scroll container, flex: 1 column with a fixed-height sibling, two plain wrappers, a flex: 1 node with a text-like measure function) laid out at 400x800, 800x400, 400x800. Asserts the measured node returns to its first height. Fails on main, passes with this change.
  • ./unit_tests Debug: all 843 tests pass. On main the new test fails (portraitHeight is 100, the relaid-out node is 60) and the other 842 pass.
  • Standalone repro (https://github.com/mozzius/rotation-text-repro, yoga-harness/run.sh) built against this branch: two and three wrapper levels now return to their first height (they stay at the landscape height on main); one wrapper level passes on both.

`computedFlexBasis` survives across layout calls, and the resolved-basis
branch of `computeFlexBasisForChild` keeps whatever is stored so a size
measured by the max-content pass carries into the definite-size pass.
That guard checks whether a value exists, not which layout produced it:
when an ancestor's measurement cache answers for a whole subtree the
skipped max-content pass leaves a basis measured against a different
available size, and a `flex: 1` node keeps its landscape height after
rotating back to portrait. Track whether a stored basis was measured,
and re-measure rather than retain a stale one from an earlier layout.

Fixes react#2019
@metkm

metkm commented Nov 2, 2026

Copy link
Copy Markdown

Hope this lands soon!. This is an annoying bug

@simongt

simongt commented Nov 3, 2026

Copy link
Copy Markdown

We hit this in a production app on React Native 0.88.0 (iOS), and #2020 fixes it for us.

Our app is portrait only, except for a full-screen photo viewer (a transparent Modal with supportedOrientations). Rotating there and back leaves the screen behind it with a flex: 1 description that keeps its landscape height, and the rows below overlap it. That made it look like a Modal bug at first, but a plain rotation round trip does the same thing.

Independent repro, core React Native only (no react-native-screens), Release builds on an iPhone 17 Pro simulator, iOS 26.5: launch in landscape, rotate to portrait, rotate back to landscape.

Build Back in landscape: flex: 1 item Row below it, y
0.88.0 prebuilt 826 x 97.3 (its portrait height) 121.3
0.86.3 prebuilt same 121
0.88.0 from source + #2020 826 x 49.3 73.3

A native probe on the stock build shows the path from #2019: the list's max-content measurement is a cache hit (826 x nan -> 826 x 122.7), then computeFlexBasisForChild keeps the item's computedFlexBasis of 97.3 from the previous generation. With #2020 applied, the same steps lay out correctly, and so does the Modal variant.

Before, stock 0.88.0:

bug-0.88.0-stock-round-trip-no-modal.mp4

After, with #2020 applied (the round trip, then the Modal variant):

fix-yoga-2020-round-trip.mp4

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stale computedFlexBasis is reused across layouts: a flex: 1 node keeps its previous size after relayout at a new root size

3 participants