Skip to content

Commit 2c68396

Browse files
authored
fix: allow className and style props to coexist when properties differ (#223)
* fix: allow className and style props to coexist when properties differ Previously, when both className and style props were provided, className-derived styles would be completely overwritten by inline styles due to Object.assign() behavior in deepMergeConfig(). This refined solution: - Only creates style arrays when className and inline styles have non-overlapping properties - Maintains CSS precedence rules (inline styles override className for same properties) - Preserves backward compatibility with existing behavior - Passes all existing tests while enabling the new functionality Enables combining NativeWind className styling with React Native Reanimated animated styles when they target different CSS properties. Fixes issue where className was ignored when style prop was also present, while preserving expected CSS specificity behavior. * perf: optimize style merging performance and readability Address code review feedback by improving the style merging logic: - Remove Set creation and array spreading for better performance - Add early exit when finding non-overlapping properties - Simplify logic flow making it more readable and maintainable - Maintain identical functionality while reducing computational overhead Performance improvements are especially beneficial for components with many style properties, reducing unnecessary object iterations.
1 parent 8a01f75 commit 2c68396

File tree

2 files changed

+83
-1
lines changed

2 files changed

+83
-1
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { render } from "@testing-library/react-native";
2+
import { Text } from "react-native-css/components/Text";
3+
import { registerCSS, testID } from "react-native-css/jest";
4+
5+
test("className with inline style props should coexist when different properties", () => {
6+
registerCSS(`.text-red { color: red; }`);
7+
8+
const component = render(
9+
<Text testID={testID} className="text-red" style={{ fontSize: 16 }} />,
10+
).getByTestId(testID);
11+
12+
// Both className and style props should be applied as array
13+
expect(component.props.style).toEqual([
14+
{ color: "#f00" }, // Changed from "red" to "#f00"
15+
{ fontSize: 16 },
16+
]);
17+
});
18+
19+
test("className with inline style props should favor inline when same property", () => {
20+
registerCSS(`.text-red { color: red; }`);
21+
22+
const component = render(
23+
<Text testID={testID} className="text-red" style={{ color: "blue" }} />,
24+
).getByTestId(testID);
25+
26+
// When same property exists, inline style should win (not array)
27+
expect(component.props.style).toEqual({ color: "blue" });
28+
});
29+
30+
test("only className should not create array", () => {
31+
registerCSS(`.text-red { color: red; }`);
32+
33+
const component = render(
34+
<Text testID={testID} className="text-red" />,
35+
).getByTestId(testID);
36+
37+
// Only className should be a flat object
38+
expect(component.props.style).toEqual({ color: "#f00" }); // Changed from "red" to "#f00"
39+
});
40+
41+
test("only inline style should not create array", () => {
42+
const component = render(
43+
<Text testID={testID} style={{ color: "blue" }} />,
44+
).getByTestId(testID);
45+
46+
// Only inline style should be a flat object
47+
expect(component.props.style).toEqual({ color: "blue" });
48+
});

src/native/styles/index.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,41 @@ function deepMergeConfig(
135135
return { ...left };
136136
}
137137

138-
let result = config.target ? Object.assign({}, left, right) : { ...left };
138+
// Handle style merging to support both className and inline style props
139+
let result: Record<string, any>;
140+
if (config.target) {
141+
if (
142+
Array.isArray(config.target) &&
143+
config.target.length === 1 &&
144+
config.target[0] === "style"
145+
) {
146+
// Special handling for style target when we have inline styles
147+
result = { ...left, ...right };
148+
// More performant approach - check for non-overlapping properties without Sets
149+
if (left?.style && right?.style && rightIsInline) {
150+
const leftStyle = left.style;
151+
const rightStyle = right.style;
152+
153+
// Quick check: do any left properties NOT exist in right?
154+
let hasNonOverlappingProperties = false;
155+
for (const key in leftStyle) {
156+
if (!(key in rightStyle)) {
157+
hasNonOverlappingProperties = true;
158+
break; // Early exit for performance
159+
}
160+
}
161+
162+
if (hasNonOverlappingProperties) {
163+
result.style = [leftStyle, rightStyle];
164+
}
165+
// Otherwise, Object.assign above will handle the override correctly
166+
}
167+
} else {
168+
result = Object.assign({}, left, right);
169+
}
170+
} else {
171+
result = { ...left };
172+
}
139173

140174
if (
141175
right &&

0 commit comments

Comments
 (0)