fix: prevent style={{}} from destroying computed styles in non-["style"] targets#312
Conversation
…e"] targets
When a component passes an empty style object (e.g., contentContainerStyle={{}})
to a non-["style"] target, mergeDefinedProps copies the empty object over the
computed className styles, destroying them. This is the same class of bug as
style={undefined} (fixed in PR nativewind#291) but for empty objects.
The ["style"] target path (Path A) already handles this correctly via
filterCssVariables({}) returning undefined. This fix extends mergeDefinedProps
with an isEmptyPlainObject check to skip empty objects on paths B and C,
matching Path A's behavior.
Common real-world trigger: components with default empty object parameters
({ contentContainerStyle = {} }) or explicit style={{}} props.
d3ecc35 to
c899098
Compare
|
Since Critical path
ApproachUses inline Tested 10 detection methods across 6 value types (50M iterations each) — Results (10M iterations, median of 5 runs)
For a form-heavy screen (20 TextInputs + 2 ScrollViews + 1 FlatList): ~50ns per frame — 0.0003% of the 16.7ms budget at 60fps. |
Summary
Extends
mergeDefinedPropsto skip empty plain objects ({}), preventing them from overwriting computed className styles on non-["style"]targets (Paths B and C).Completes the work started in PR #291 (which fixed
style={undefined}) by also handlingstyle={{}}.Problem
When a component passes an empty style object to a non-
["style"]target, the computed className styles are destroyed:Common real-world trigger — components with default empty object parameters:
The
["style"]target path (Path A) already handles this viafilterCssVariables({})returningundefined. Paths B and C (non-["style"]array targets like["contentContainerStyle"]and string targets) usedmergeDefinedPropswhich copied empty objects.Solution
Uses inline
for...into detect empty objects — if the loop body never executes, the object has no enumerable keys and is skipped. No intermediate array allocation (unlikeObject.keys().length), no function call overhead (unlike a separate helper).Performance
mergeDefinedPropsis only called for Path B/C components (ScrollView, FlatList, TextInput, KeyboardAvoidingView, ImageBackground). The most common components (View, Text, Pressable) use Path A and are unaffected.Benchmarked with 10M iterations (median of 5 runs) across real NativeWind patterns:
<TextInput className="..." />(className only, most common)<TextInput className="..." style={{ height: 40 }} /><ScrollView contentContainerClassName="..." /><FlatList contentContainerStyle={{}} />(the bug)Frame impact for a heavy screen (20 TextInputs + 2 ScrollViews + 1 FlatList): ~50ns per frame (0.0003% of 16.7ms budget at 60fps).
for...inwas chosen over 9 other approaches after benchmarking 50M iterations each.Object.keys().lengthwas 2x slower for empty objects and allocates an intermediate array. A targeted approach (checking only the style key) was slower due to string comparison overhead.Verification
yarn typecheckyarn lintyarn buildyarn testBug reproduction: without fix 4 tests fail, with fix all 19 className-with-style tests pass.
Tests added (5 new)
style={{}}contentContainerStyle={{}}contentContainerStyle={{}}style={{}}= {}defaultRelated: #239, #291