You are building a reusable React Native CouponCard with a disabled variant; how should it type, order and merge a caller's style prop, given there are no selectors?
answer
- StyleProp<ViewStyle> from the package root
- defaults, variant, caller - in that order
- one style prop per styled child
- never object-spread a style prop
- flatten to read, compose to keep identity
basics
~20 sType the prop as StyleProp<ViewStyle> from react-native, merge it as the last array entry after the base and disabled styles, expose extra props for inner elements since selectors do not exist, and never spread a style prop into an object.
solid answer
~50 sAccept `style?: StyleProp<ViewStyle>` imported from `react-native` itself - deep imports are a type error under 0.87's strict TypeScript API - so callers can pass an object, an array or a falsy value. Merge with an array: `[styles.card, disabled && styles.cardDisabled, style]`. Order is the policy: last wins, so the caller can override anything - fine for margins and width, risky for the disabled look. If the disabled appearance must not be overridden, put it **after** `style`, or keep `style` for outer layout only. Since there are no selectors, expose a separate prop per inner element that callers may style, such as `titleStyle?: StyleProp<TextStyle>`. Never write `{ ...styles.card, ...style }`: if `style` is an array, the spread copies index keys, not properties. Use `StyleSheet.flatten` only to read a resolved value, and `StyleSheet.compose` when a stable reference matters.
code
tsx · 25 linesimport { StyleSheet, Text, View } from 'react-native';
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';
type Props = {
title: string;
disabled?: boolean;
style?: StyleProp<ViewStyle>; // outer layout: margins, width
titleStyle?: StyleProp<TextStyle>;
};
export function CouponCard({ title, disabled = false, style, titleStyle }: Props) {
return (
// Caller's style first, disabled last: the disabled look cannot be overridden.
<View style={[styles.card, style, disabled && styles.cardDisabled]}>
<Text style={[styles.title, titleStyle, disabled && styles.titleDisabled]}>{title}</Text>
</View>
);
}
const styles = StyleSheet.create({
card: { padding: 16, borderRadius: 12, backgroundColor: '#f3fbf6' },
cardDisabled: { opacity: 0.5 },
title: { fontSize: 18, fontWeight: '600' },
titleDisabled: { textDecorationLine: 'line-through' },
});go deeper
Know that a reusable component takes a style prop and merges it into a style array with its own styles.
Type style props with StyleProp, explain why order sets the override policy, and why an object spread breaks on arrays.
Design the component's style contract: which elements are stylable, whether the disabled look can be overridden, and how merged styles stay stable for memoized children.
Set the library-wide rule for style props and variants, trading caller flexibility against design consistency, and hold every shared component to it in review.
## The problem a style prop solves In CSS, a consumer can restyle a component's internals from outside with a selector such as `.wallet .coupon-title`. React Native has **no selectors and no cascade**: a style reaches an element only through that element's `style` prop. A reusable `CouponCard` therefore has to decide, in its props, exactly what callers may style and how their styles combine with its own - including its disabled variant for expired or redeemed coupons. ## Typing - Use **`StyleProp<ViewStyle>`** for the container and **`StyleProp<TextStyle>`** or **`StyleProp<ImageStyle>`** for inner elements. `StyleProp<T>` is `T`, a nested array of `T` and falsy values, or a falsy value - the same shapes the core components accept - so callers can pass `[a, cond && b]` straight through. - Import these types from **`react-native`**. React Native 0.87's strict TypeScript API makes deep imports from `react-native/Libraries/...` a type error. - Avoid `ViewStyle` alone as the prop type: it rejects arrays and falsy values, forcing callers to flatten. ## Ordering is the override policy The resolved style comes from walking the array left to right, last entry winning per property. The order you choose is the component's contract: | Order | Caller can override disabled look? | Typical use | |---|---|---| | `[styles.card, disabled && styles.cardDisabled, style]` | yes | caller trusted with appearance | | `[styles.card, style, disabled && styles.cardDisabled]` | no - disabled wins | design system enforcing the disabled look | | `[styles.card, layoutStyle, disabled && styles.cardDisabled]` with a narrow type | only layout props | stricter libraries | Pick one deliberately and document it. A common bug is a caller passing `style={{ opacity: 1 }}` to fix an unrelated fade, which silently cancels the disabled `opacity: 0.5` under the first ordering. ## Styling inner elements without selectors Because a style set on the card's `View` never reaches the title `Text`, expose a prop per element that callers legitimately need to adjust: 1. `style` - the outer container. 2. `titleStyle` - the coupon title. 3. `badgeStyle` - the discount badge. Each merges the same way, with the same `disabled` flag driving each element's variant entry. Resist exposing every inner element; each prop is API surface you must keep working. ## Merging correctly - **Arrays, not spreads.** `{ ...styles.card, ...style }` works only when `style` is a single object. If a caller passes an array, spreading it copies keys `0`, `1` and so on, which are not style properties, and the caller's styles vanish. A style array always works. - **`StyleSheet.compose(styles.card, style)`** returns `styles.card` itself when `style` is `null` or `undefined`, avoiding a new array each render. It matters when the merged style feeds a memoized child. - **`StyleSheet.flatten(style)`** is for reading, for instance when the card needs the caller's effective `borderRadius` to round an inner image. The result may be the caller's own object, so never mutate it. ## Where the variant's styles live Declare the static base and disabled styles once with `StyleSheet.create` at module scope. Colours that change with the app's theme are a separate concern: keep static layout in module-scope styles and supply theme colours as additional entries, rather than rebuilding the whole sheet per render. ## A review checklist - Is every style prop typed with `StyleProp<...>` from `react-native`? - Is the merge order a documented decision, especially relative to the disabled variant? - Does each inner element callers may style have its own prop? - Is there any object spread of a style prop? - Is `flatten` used only to read, and never mutated?
- Why type a style prop as StyleProp<ViewStyle> rather than ViewStyle?Because `ViewStyle` accepts only a single object, while callers routinely pass arrays and conditional entries such as `[styles.a, active && styles.b]`. `StyleProp<ViewStyle>` accepts a style object, nested arrays of them and falsy values - the same shapes the core components take - so the prop can be forwarded straight into a style array without flattening.
- A caller's style={[styles.wide, styles.raised]} has no effect on a card that merges with { ...styles.card, ...style }. Why?Spreading an array into an object literal copies its index keys, so the merged object gains keys `0` and `1` holding the caller's style objects instead of their properties. Those keys are not style properties, so the caller's styles are lost. Merging with a style array - `[styles.card, style]` - handles objects, arrays and falsy values alike.
saying these in an interview costs you the question
- Callers can restyle a component's inner Text with a descendant selector.
- Typing the prop as ViewStyle is enough, because callers never pass arrays.
- { ...styles.card, ...style } is a safe way to merge any caller style.
- Deep-importing style types from react-native/Libraries is fine in 0.87.
- The caller's style must always come last in the array.