skip to content

In React Native's Fabric renderer, what is view flattening, and when do you need collapsable={false} on a View?

level: middleimportance: nice to knowfreq 25%

answer

  1. views that only arrange children
  2. decided during mount's diff
  3. no longer Android-only
  4. testID or background keeps a view
  5. native code needs the real view

basics

~10 s

View flattening lets Fabric skip creating native views for layout-only Views, folding their layout into their children during diffing. Set collapsable={false} when native code needs that View to exist as a real platform view.

solid answer

~50 s

Many `View`s only arrange their children with margin, padding or flex. During the mount phase's diff, Fabric's C++ core detects these **layout-only** views and does not create host views for them, folding their layout into their children's positions; the screen looks identical but the native hierarchy is shallower. It runs on every platform, whereas the legacy renderer only did it on Android. A `View` keeps its host view when a prop requires one: a background, a border, opacity, a transform, `testID`, `nativeID`, accessibility props, touch or responder handlers. `collapsable` defaults to `true`; setting it to `false` forces a host view, which you need when native code must find or operate on that platform view, such as a snapshot or native gesture library. Measuring from JavaScript does not need it, because Fabric measures the shadow tree.

code

tsx · 14 lines
tsx
import { useRef } from 'react';
import { Text, View, type ViewInstance } from 'react-native';

export function SnapshotTarget({ title }: { title: string }) {
  // Handed to native code that captures the platform view as an image.
  const captureRef = useRef<ViewInstance>(null);

  return (
    // Only padding: without collapsable={false} Fabric may flatten it away.
    <View ref={captureRef} collapsable={false} style={{ padding: 12 }}>
      <Text>{title}</Text>
    </View>
  );
}

go deeper

for a junior

Know that React Native can drop wrapper Views that only do layout, and that collapsable={false} keeps one.

for a middle

Explain that flattening happens during mount's diff in C++, which props force a host view, and why it now also applies on iOS.

for a senior

Diagnose native integrations that lost their view after an upgrade, and apply collapsable={false} narrowly instead of disabling flattening broadly.

for a principal

Balance the memory and layout savings of shallow native hierarchies against the needs of native libraries and test tooling that expect stable views.

## The problem: layout-only views Composition in React Native produces a lot of `View`s that exist only to arrange their children: a wrapper with a margin, a row with padding, a container that centers something. They draw nothing themselves. Call them **layout-only views**. If each one became a real native view, a screen would carry a deep host hierarchy full of views that cost memory and work to create, lay out and traverse, without changing a single pixel. ## View flattening **View flattening** is the Fabric renderer's answer. During the **diffing** step of the mount phase, the C++ core notices shadow nodes that only affect layout and does not create host views for them. Their layout contribution (for example a `margin: 10`) is folded into the position of their children, and the children are mounted directly into the nearest ancestor that does produce a view. - It happens as part of diffing, so it adds no separate pass. - It is written once in C++ and runs on every platform. The legacy renderer only flattened on Android; Fabric brought it to iOS. - It is invisible on screen: the user sees exactly the same result. ## What keeps a View from being flattened A `View` gets a host view of its own when any of its props means it must exist natively. Reading the renderer's source, that includes: | Kind of prop | Examples | |---|---| | Drawing | a meaningful `backgroundColor`, a border, `boxShadow`, `opacity` other than 1 | | Transforms and stacking | `transform`, `zIndex` on a non-static view, `overflow` other than `visible` | | Identity | `nativeID`, `testID` | | Accessibility | `accessible`, `importantForAccessibility` other than `auto`, accessibility modal or hidden flags | | Events and interaction | touch, pointer or responder handlers on the view, `pointerEvents` of `none` or `box-only` | | Explicit opt-out | `collapsable={false}` | A view with only `margin`, `padding` or flex properties is a candidate for flattening. ## The collapsable prop `collapsable` is a `View` prop that defaults to `true`. Setting `collapsable={false}` tells the renderer to always keep a host view for that `View`. A related prop, `collapsableChildren={false}`, applies the same protection to the view's direct children. You need it when **native code expects a real platform view to exist**: 1. A native library receives the view by its tag or ref and operates on the platform view, for example to capture it as an image or attach a native gesture recognizer to it. 2. Native code walks the platform view hierarchy and expects a specific parent-child structure. 3. A parent native component expects a fixed number of child views (for example one host view per page) and a layout-only wrapper would otherwise vanish. ## What does not need it - **Measuring from JavaScript.** Fabric's `measure`, `measureInWindow` and `getBoundingClientRect` compute results from the shadow tree's layout, not from the platform view, so measuring a layout-only `View` does not by itself require `collapsable={false}`. - **Styling.** Adding a background or border already produces a view. - **Performance tuning in general.** Flattening is a performance win; disabling it everywhere just rebuilds the deep hierarchy it removes. ## A debugging story A view-snapshot feature in a note-taking app captures a `View` that wraps a note card with only `padding`. After moving to the New Architecture, capture fails on iOS: the wrapper no longer exists as a native view, because Fabric now flattens on iOS too. The fix is one prop on the wrapper, `collapsable={false}`. The same reasoning applies whenever a native integration broke only after the renderer changed. ## Summary Flattening removes host views for layout-only `View`s during diffing, on every platform. Props that draw, transform, identify, handle events or expose accessibility keep a view. `collapsable={false}` is the explicit escape hatch for native code that needs the platform view to exist.

  • In React Native's Fabric renderer, why did some native integrations break on iOS only after moving to the New Architecture?
    The legacy renderer flattened layout-only views only on Android, so on iOS every View had a native view. Fabric flattens on both platforms, so a wrapper that native code relied on, for example to snapshot or attach a recognizer, can disappear from the iOS hierarchy. Adding collapsable={false} to that wrapper restores its host view.
  • Does adding testID to a React Native View affect view flattening?
    Yes. In Fabric's source, a non-empty testID is one of the props that makes a View form a host view, alongside nativeID, backgrounds, borders, opacity, transforms and accessibility props. That is intentional, since test tools need to find a real view, but it means sprinkling testID on every wrapper also turns off flattening for them.

saying these in an interview costs you the question

  • View flattening changes what the user sees on screen.
  • Flattening still only happens on Android under Fabric.
  • Every View with a ref is automatically kept as a host view.
  • You must set collapsable={false} to measure a View from JavaScript.
  • collapsable defaults to false, so flattening is opt-in.