skip to content

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?

level: seniorimportance: should knowfreq 30%

answer

  1. StyleProp<ViewStyle> from the package root
  2. defaults, variant, caller - in that order
  3. one style prop per styled child
  4. never object-spread a style prop
  5. flatten to read, compose to keep identity

basics

~20 s

Type 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 s

Accept `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 lines
tsx
import { 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

for a junior

Know that a reusable component takes a style prop and merges it into a style array with its own styles.

for a middle

Type style props with StyleProp, explain why order sets the override policy, and why an object spread breaks on arrays.

for a senior

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.

for a principal

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.