Add useTraitHiddenOnIOS feature flag#172
Conversation
## Summary: Mirrors useTraitHiddenOnAndroid from facebook#54112. Lets iOS apps opt out of the `Trait::Hidden` slice-skip; Android can already opt in. Default is `true`, preserving today's iOS behavior. What the optimization does D22134220 (2020, "Fabric: display: none nodes do not create views anymore") added `Trait::Hidden` and a slice-skip in `sliceChildShadowNodeViewPairs` that filters out subtrees whose Yoga display is None. On iOS the diff then issues REMOVE + DELETE for the entire subtree — invisible content stops costing anything. For most UIs this is a clean win. Where it bites The optimization assumes that `display: none` transitions are rare. That breaks for a specific pattern: **a custom host component mounted under `<Suspense>`**. Each suspend → resume cycle, Suspense flips display between `flex` and `none`. With the optimization on, every cycle: - tears down the entire subtree of UIViews, - drops per-instance native state — measurement caches, scroll position, animation drivers, anything internal to the host component, - re-runs `init`/`dealloc` and rebuilds the subtree on resume. Suspend/resume ends up heavier than a fresh mount, and any state the component held disappears between renders. What the flag does **It re-activates an existing code path. Nothing in the mounting layer is new.** The hide-via-`UIView.hidden` wiring shipped in D8460108 (June 2018, "Fabric: Default support of displayType and layoutDirection layout...") and has lived in `UIView+ComponentViewProtocol updateLayoutMetrics:` ever since — a few lines below the slice consumer in the same file. For two years it was the only iOS path; the 2020 slice-skip didn't replace it, it just made it unreachable in the common case. Setting the flag to `false` lets Hidden shadow nodes pass through the slice. The differ emits an `UPDATE_LAYOUT_METRICS` mutation with `displayType == None`, and the 2018 wiring picks it up: `self.hidden = YES` on the underlying UIView. Same view, hidden in place. Defaults - `useTraitHiddenOnIOS = true` — keeps the iOS behavior introduced in 2020. - `useTraitHiddenOnAndroid = false` — keeps the Android behavior, which never adopted the slice-skip. Both flags share the same semantic ("use the optimization"); the defaults encode each platform's pre-flag behavior. Flipping either default is out of scope. ## Changelog: [IOS] [ADDED] - useTraitHiddenOnIOS feature flag to opt out of the `display: none` slice-skip optimization ## Test Plan: No new tests. `StackingContextTest` exercises the slice-skip path and passes unchanged with the flag at its default `true`. Manually flipping the flag to `false` produces the existing Android branch's expected view tree (8 views, Hidden subtrees preserved with `self.hidden = YES`).
| // On iOS, gated by useTraitHiddenOnIOS. When false, the view stays in | ||
| // the slice and is hidden via UIView.hidden = YES in | ||
| // updateLayoutMetrics: instead of being removed. | ||
| if (ReactNativeFeatureFlags::useTraitHiddenOnIOS() && |
There was a problem hiding this comment.
this is the only important code of this PR
| // updateLayoutMetrics: instead of being removed. | ||
| if (ReactNativeFeatureFlags::useTraitHiddenOnIOS() && | ||
| childShadowNode.getTraits().check(ShadowNodeTraits::Trait::Hidden)) { | ||
| continue; |
There was a problem hiding this comment.
continue on line 67 drops this node from the mount slice.
The next frame's diff against the previous slice will then see the subtree (of our frozen child) as missing and emit REMOVE + DELETE instructions - destroying the underlying UIView and all the children it has (chat and messages etc).
Old arch didn't have this (questionable) optimization, and children only hid - which is what we want to make everything work again. Instead without this, react-native just hides the view which is what we want.
hannojg
left a comment
There was a problem hiding this comment.
lgtm, lets go!
(I was thinking at first it would be easier to just CP the commit from 0.83 and remove the #ifdef android directive, but i think its cleaner to have this separately for iOS!)
Yeah I wanted to
|
|
also upstreamed it on RN repo - issue opened here and suggested PR here |
Summary:
Adds
useTraitHiddenOnIOSfeature flag - the iOS twin of Meta'suseTraitHiddenOnAndroid(#54112). Defaults totrueso iOS behavioris unchanged for anyone who doesn't override it.
Both the chat empty on channel re entry bug and that chat to non-chat bug that we've been chasing come from
iOS Fabric tearing down Suspense-frozen subtrees on every freeze
cycle.
DCDChatComponentView(and its entire row cache, scrollposition, etc.) gets destroyed and rebuilt every time a chat surface
goes under
display: none.RN's been carrying the wiring to hide views instead of destroying
them since 2018. A 2020 optimization started filtering hidden subtrees out of the mounting slice before they could reach that path.
This flag re-opens it on demand.
Next
falsein ourRCTAppDelegateand ships the fix.What's in this PR
sliceChildShadowNodeViewPairs.cpp(the only important code change).ReactNativeFeatureFlags.config.js,defaultValue: trueplus ~20 auto-generated files needed for the flagUpstream
Designed to go straight to
facebook/react-nativeonce it's stablehere.
TODO when we upgrade RN to 0.83
RN 0.83 (which ships
useTraitHiddenOnAndroid(facebook#54112), if we upgrade we should drop this#ifndef ANDROIDline and inline both flags into the gate using the following structure:Which is also the code I'll upstream to RN
Behavior is unchanged either way - the rewrite is purely cosmetic