Fabric/iOS: strokeDasharray cannot be cleared on a recycled view (missing else in RNSVGFabricConversions.h)
Description
On Fabric/iOS, strokeDasharray is the one stroke property that cannot be cleared on a recycled view. When React reuses a native RNSVGCircle (or any renderable) for an element whose new props omit strokeDasharray, the view keeps the dash pattern from its previous occupant, and a solid stroke renders dashed.
The cause is a missing else in apple/Utils/RNSVGFabricConversions.h:
id strokeDasharray = RNSVGConvertFollyDynamicToId(renderableProps.strokeDasharray);
if (strokeDasharray != nil) {
renderableNode.strokeDasharray = [RCTConvert RNSVGLengthArray:strokeDasharray];
}
// no else — a previously-set dasharray is never resetMost neighbouring stroke props in the same function assign unconditionally, so they self-clear on recycle: stroke, strokeOpacity, strokeLinecap, strokeLinejoin, strokeDashoffset, strokeMiterlimit. Only strokeWidth and strokeDasharray are wrapped in a nil-check with no else branch, and strokeWidth is far less visible because it is rarely toggled between "set" and "unset" on the same element.
There is no JS-side value that clears it either
This is the part that makes it a genuine dead end rather than an inconvenience, and it is worth stating explicitly because "none" is the obvious workaround to reach for:
- Omitting the prop —
src/lib/extract/extractStroke.tsguards withif (strokeDasharray != null), so the key is never written onto the props object and nothing reaches native. strokeDasharray="none"— the guard passes, but the branch resolves tonull:Thatconst strokeDash = !strokeDasharray || strokeDasharray === 'none' ? null : extractLengthList(strokeDasharray);nullarrives asfolly::dynamic::NULLT, andRNSVGConvertFollyDynamicToIdreturnsnilforNULLT— so the nativeif (… != nil)is false and the assignment is skipped again."none"is indistinguishable from omitting the prop.strokeDasharray="0"— reaches native (extractLengthList('0')→[0], odd-length, concatenated to[0, 0]), so it does overwrite the stale value. Whether CoreGraphics renders an all-zero pattern as solid is a separate question; the point is it is the only input that reaches the setter at all.
So from JS there is no clean way to express "this element has no dash."
Steps to reproduce
Render two Circles swapped by a ternary, where only one sets strokeDasharray, with no key on either branch (a keyless fragment is unwrapped, so index 0 matches by type and the native view is recycled):
const [dashed, setDashed] = useState(true);
<Svg width={120} height={120}>
{dashed ? (
<Circle cx={60} cy={60} r={50} stroke="#888" strokeWidth={6} fill="none" strokeDasharray="1 6" />
) : (
<Circle cx={60} cy={60} r={50} stroke="#2a7" strokeWidth={6} fill="none" />
)}
</Svg>
<Button title="toggle" onPress={() => setDashed(d => !d)} />Toggle once.
Expected: the second circle draws a solid stroke.
Actual: it draws with 1 6 dashing inherited from the first. Adding distinct keys to the two branches fixes it, which confirms recycling is the trigger.
Impact
This is easy to ship without noticing. In our case a dashed "unknown" ring and a solid "logged" ring shared a position in a week strip; the defect existed for weeks while the dash pattern was dense (2 3.5, which read as solid), and only became visible when the pattern was changed to a sparser 1 6. The rendering was wrong the whole time.
Suggested fix
Give the two guarded properties an else that resets to the default, matching what the unconditional assignments in the same function already do:
if (strokeDasharray != nil) {
renderableNode.strokeDasharray = [RCTConvert RNSVGLengthArray:strokeDasharray];
} else {
renderableNode.strokeDasharray = nil;
}RNSVGRenderable's draw path already guards on strokeDasharray.count, so a nil array is handled correctly downstream.
I'm happy to open a PR if this framing looks right.
Version info
| package | version |
|---|---|
| react-native-svg | 15.15.4 |
| react-native | 0.85.3 |
| architecture | Fabric / New Architecture |
| platform | iOS |
Source: software-mansion/react-native-svg