In React Native, what do StyleSheet.flatten and StyleSheet.compose return, and when would you use each instead of a plain style array?
answer
- flatten: one object, last wins
- a single object comes back as-is
- compose: two styles, maybe no array
- null or undefined returns the other
- flatten to read, not to render
basics
~20 sStyleSheet.flatten resolves a style or nested array into one object where later entries win, returning a lone object unchanged. StyleSheet.compose(a, b) returns [a, b], or just the other style when one is null or undefined.
solid answer
~50 s`StyleSheet.flatten(style)` resolves a style prop into **one object**: it walks nested arrays left to right, skips anything that is not an object, and assigns each key so the last entry wins. Two edge cases matter: a single non-array object is returned **as the same reference**, not a copy, and a falsy input returns `undefined`. Use it when you need to **read** a resolved value - the effective `borderRadius` of a caller's style, or a plain object for an API that cannot take arrays. `StyleSheet.compose(style1, style2)` combines two styles so `style2` overrides `style1`; if either is `null` or `undefined`, it returns the other one without allocating an array, which keeps references stable for memoized children. Neither is needed just to render: a style array already works, and flattening in render allocates a new object each time.
code
tsx · 24 linesimport { Image, StyleSheet, View } from 'react-native';
import type { StyleProp, ViewStyle } from 'react-native';
type Props = { imageUri: string; style?: StyleProp<ViewStyle> };
export function CouponCard({ imageUri, style }: Props) {
// Read-only: flatten may return the caller's own (possibly frozen) object.
const radius = StyleSheet.flatten(style)?.borderRadius ?? styles.card.borderRadius;
return (
// compose returns styles.card itself when no style prop is passed.
<View style={StyleSheet.compose(styles.card, style)}>
<Image
source={{ uri: imageUri }}
style={[styles.header, { borderTopLeftRadius: radius, borderTopRightRadius: radius }]}
/>
</View>
);
}
const styles = StyleSheet.create({
card: { borderRadius: 12, backgroundColor: '#ffffff' },
header: { height: 96 },
});go deeper
Know that flatten turns a style array into one object where later entries win, and that compose combines two styles with the second overriding the first.
Explain flatten's same-reference return for a single object, compose's null-or-undefined short-circuit, and why neither is needed just to render.
Use flatten only to read resolved values, keep style props stable with compose on memoized paths, and catch mutation of shared or frozen styles.
Keep style helpers out of hot render paths by default, and make component APIs that need resolved values say so explicitly instead of flattening everywhere.
## Three ways to combine styles In React Native a component's `style` prop can be an object, a nested array or a falsy value, and the renderer resolves an array itself. `StyleSheet` adds two helpers on top of that: | Helper | Input | Output | |---|---|---| | plain array | any number of entries | the array itself, resolved by the renderer | | `StyleSheet.flatten` | any style prop | one resolved object, or `undefined` | | `StyleSheet.compose` | exactly two styles | one of them, or a two-item array | ## StyleSheet.flatten The React Native 0.87 implementation behaves as follows: 1. If the input is `null` or not an object - `undefined`, `false`, a number - it returns **`undefined`**. 2. If the input is a single object (not an array), it returns **that same object**. No copy is made. 3. If the input is an array, it creates a new object and walks the entries left to right, recursing into nested arrays and skipping entries that are not objects, assigning every key it finds. Later entries overwrite earlier keys. The docs' example shows the result: flattening `[styles.listItem, styles.selectedListItem]` where both set `color` returns one object carrying the second entry's colour. The merge is **shallow**: an object-valued property such as `transform` is replaced whole. The second rule is the trap. Code like `const s = StyleSheet.flatten(props.style); s.opacity = 0.4;` mutates the caller's object when the caller passed a single style - and if that style came from `StyleSheet.create`, it is frozen in development and the assignment throws. Treat the result as read-only, or copy it first. ### When flatten earns its place - **Reading a resolved value.** A coupon card clips its header image and needs the caller's effective `borderRadius`; `StyleSheet.flatten(style)?.borderRadius` gives the value the renderer would use. - **Handing a plain object to code that cannot take arrays**, such as a function that computes a derived colour or a library that expects a single style object. - **Tests** that assert a component's resolved style. It is **not** needed to render: passing the array is enough, and flattening in the render body allocates a fresh object every render, which also changes the prop's identity. ## StyleSheet.compose `StyleSheet.compose(style1, style2)` combines two styles so that `style2` overrides `style1`. Its implementation is three lines: - if `style1` is `null` or `undefined`, return `style2`; - if `style2` is `null` or `undefined`, return `style1`; - otherwise return `[style1, style2]`. The point is **allocation and identity**. In a coupon card that accepts an optional `style` prop, `StyleSheet.compose(styles.card, style)` returns `styles.card` itself when no override is passed - the same reference every render - instead of a fresh `[styles.card, undefined]` array. A memoized child that receives it can skip re-rendering. One precision detail: the docs say "if either style is falsy", but the 0.87 source checks only for `null` and `undefined`. `StyleSheet.compose(isDisabled && styles.cardDisabled, style)` with `isDisabled` false therefore returns `[false, style]`, which still renders correctly because the `false` entry is skipped, but no longer saves the allocation. Pass `undefined` rather than `false` when the saving matters: `isDisabled ? styles.cardDisabled : undefined`. ## Cost `flatten` on an array allocates a new object and walks every entry on each call. Reading one value per render is cheap, but flattening large nested arrays inside list rows adds up; compute the value once with `useMemo` keyed on the style prop, or restructure the API so the component receives the value it needs directly. `compose` allocates at most one two-item array and nothing when an argument is absent. ## Choosing - **Rendering with conditions:** a plain array, `[styles.card, isDisabled && styles.cardDisabled, style]`. - **One base plus one optional override on a hot path:** `StyleSheet.compose`. - **Needing a value out of a style:** `StyleSheet.flatten`, read-only.
- Why is mutating the result of StyleSheet.flatten(props.style) dangerous?Because when `props.style` is a single object, `flatten` returns that same object rather than a copy. The mutation then changes the caller's style for every component sharing it, and if the style came from `StyleSheet.create` it is frozen in development, so the assignment throws a `TypeError` there while silently succeeding in release. Copy the result before changing it.
- What does StyleSheet.compose(false, styles.card) return in React Native 0.87?The array `[false, styles.card]`. The implementation short-circuits only when an argument is `null` or `undefined`, even though the docs say "falsy". The result renders correctly because the `false` entry is skipped, but the call allocates an array and loses the reference-equality benefit. Pass `undefined` for an absent style when that matters.
saying these in an interview costs you the question
- StyleSheet.flatten always returns a new object you can safely mutate.
- You must flatten a style array before passing it to a component's style prop.
- StyleSheet.compose deep-merges its two styles into one new object.
- StyleSheet.compose(false, style) returns style without allocating an array.
- StyleSheet.flatten lets the first entry win when keys conflict.