Skip to content

[Fabric] Changing a view from a fixed width to flex: 1 keeps the old width (stale Yoga computedFlexBasis) #59741

Open

Description

@wil-li-la

Description

On the New Architecture, when a view's style changes from a definite main-axis size (e.g. width: 300 in a row) to a style that resolves a flex basis (flex: 1, i.e. basis 0), the view keeps its previous width as its flex basis. Because RN's flex: 1 has flexShrink: 0, the view then never shrinks: in a container narrower than the old width it overflows. In a wider container flexGrow hides the problem.

We hit it in a two-pane layout: the list pane is width: 475 while two panes are shown and flex: 1 when only one is. When the window shrank to 469 pt (iPhone Duo, two apps side by side) the list stayed 475 pt wide and overflowed the window. React sends the right update to native (cloneNodeWithNewChildrenAndProps with {"flex":1,"width":null,...}), but Yoga lays the node out at the old width and no further onLayout fires.

Cause

YogaLayoutableShadowNode::updateYogaProps() applies a changed style with yogaNode_.setStyle(...) + yogaNode_.setDirty(true). Yoga's own style setters (YGNodeStyleSet*) call Node::markDirtyAndPropagate(), which also clears layout.computedFlexBasis; Node::setDirty() does not. In computeFlexBasisForChild, the resolved-flex-basis branch only computes a basis when computedFlexBasis is undefined (unless WebFlexBasis is on), so the basis cached while the node had width: 300 is reused after the style becomes flex: 1.

This code is unchanged in v0.88.1 and on main.

Related but different: #58294 / react/yoga#2019 (fix proposed in react/yoga#2020). There the node's style never changes; a basis measured in an earlier layout survives because an ancestor's measurement cache skips the subtree on rotation. Here the node's own style changes, and the stale value is the basis resolved from its old width (the style-dimension branch). react/yoga#2020 marks that kind of basis as not measured and keeps retaining it, so it does not cover this case.

Suggested fix

In YogaLayoutableShadowNode::updateYogaProps(), when the style changed, also clear the cached basis, matching what markDirtyAndPropagate() does:

const bool styleChanged = styleResult != yogaNode_.style();
if (!YGNodeIsDirty(&yogaNode_) && styleChanged) {
  yogaNode_.setDirty(true);
}
if (styleChanged) {
  yogaNode_.setLayoutComputedFlexBasis(yoga::FloatOptional());
}
yogaNode_.setStyle(styleResult);

Happy to open a PR.

Yoga-only reproduction

The same thing happens with RN's vendored Yoga alone when the style is applied the way Fabric applies it (setStyle() + setDirty()):

#include <yoga/Yoga.h>
#include <yoga/node/Node.h>
#include <cstdio>
using namespace facebook::yoga;

int main() {
  YGNodeRef root = YGNodeNew();
  YGNodeStyleSetFlexDirection(root, YGFlexDirectionRow);
  YGNodeStyleSetWidth(root, 951);
  YGNodeStyleSetHeight(root, 600);
  YGNodeRef list = YGNodeNew();
  YGNodeStyleSetWidth(list, 475);
  YGNodeInsertChild(root, list, 0);
  YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR);

  // What Fabric does on a React commit: new styles via setStyle() + setDirty().
  Node* r = resolveRef(root);
  Node* l = resolveRef(list);
  Style rs = r->style();
  rs.setDimension(Dimension::Width, StyleSizeLength::points(469));
  r->setStyle(rs);
  r->setDirty(true);
  Style ls = l->style();
  ls.setDimension(Dimension::Width, StyleSizeLength::undefined());
  ls.setFlex(FloatOptional(1));
  l->setStyle(ls);
  l->setDirty(true);
  YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR);

  printf("list width = %.0f (row = 469)\n", YGNodeLayoutGetWidth(list));  // prints 475
}
How the style is applied row 469 row 385 row 669
setStyle() + setDirty() (Fabric today) 475 475 669
YGNodeStyleSetWidth(YGUndefined) + YGNodeStyleSetFlex(1) 469 385 669
Fabric path + setLayoutComputedFlexBasis(FloatOptional()) 469 385 669

Steps to reproduce

  1. Clone the reproducer and run it (yarn install, then yarn android or yarn ios).
  2. Each row starts 360 wide with a 300-wide red child. Press Shrink rows to 200. This shrinks both rows to 200 in one commit and changes the children's style:
    • top row: width: 300 → flex: 1
    • bottom row (control): width: 300 → width: '100%'
  3. Expected: both children fit the row (196 inside the 2 pt border).
    Actual: the flex: 1 child stays 300 wide and overflows the 200 row; the control child is 196. Pressing the button again (back to 360) and once more reproduces it every time.

React Native Version

0.88.1

Affected Platforms

Runtime - Android, Runtime - iOS

Areas

Fabric - The New Renderer

Output of npx @react-native-community/cli info

System:
  OS: macOS 27.0.1
  CPU: (10) arm64 Apple M1 Pro
  Memory: 66.84 MB / 32.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 22.23.0
    path: ~/.nvm/versions/node/v22.23.0/bin/node
  Yarn:
    version: 1.22.22
    path: /opt/homebrew/bin/yarn
  npm:
    version: 10.9.8
    path: ~/.nvm/versions/node/v22.23.0/bin/npm
  Watchman:
    version: 2025.10.27.00
    path: /opt/homebrew/bin/watchman
Managers:
  CocoaPods:
    version: 1.16.2
    path: /opt/homebrew/bin/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 27.2
      - iOS 27.2
      - macOS 27.2
      - tvOS 27.2
      - visionOS 27.2
      - watchOS 27.2
  Android SDK:
    API Levels:
      - "36"
      - "37"
    Build Tools:
      - 35.0.0
      - 36.0.0
      - 36.1.0
      - 37.0.0
    System Images:
      - android-36.1 | Google Play ARM 64 v8a
    Android NDK: Not Found
IDEs:
  Android Studio: 2025.1 AI-251.26094.121.2513.14007798
  Xcode:
    version: 27.2/27B52
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 21.0.7
    path: /usr/bin/javac
  Ruby:
    version: 2.6.10
    path: /usr/bin/ruby
npmPackages:
  "@react-native-community/cli":
    installed: 20.2.0
    wanted: 20.2.0
  react:
    installed: 19.3.0
    wanted: 19.3.0
  react-native:
    installed: 0.88.1
    wanted: 0.88.1
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: true
  newArchEnabled: true
iOS:
  hermesEnabled: Not found
  newArchEnabled: false

(Run on the Android reproducer; the iOS section shows defaults because Pods were not installed for this run.)

Stacktrace or Logs

No crash. Values printed by the reproducer after pressing the button:
row 200, child 300: BUG (child wider than row)   <- width: 300 -> flex: 1
row 200, child 196: OK                           <- width: 300 -> width: '100%' (control)

MANDATORY Reproducer

https://github.com/wil-li-la/rn-stale-flex-basis-repro

Screenshots and Videos

Android emulator (API 36.1), RN 0.88.1 release build. Left: before. Right: after Shrink rows to 200.

before and after

We first saw it on iOS (iOS 27.1 simulator, RN 0.81.5 via Expo SDK 54, New Architecture). The reproducer above was verified on Android with 0.88.1; the code involved is shared C++ and unchanged between those versions.

Activity

  1. dave-otf commented on Nov 2, 2026

    @dave-otf

    The table separates this from the measured-basis case in #58294/#2020. A regression test for the Fabric setStyle() + setDirty() path using the two-row width-to-flex: 1 repro would pin it: after the row shrinks to 200, the flex child should fit inside the bordered row like the 100% control, then still grow when restored to 360.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions