skip to content

Reviewing a React Native weather app's header, you find Platform.OS ternaries on most style properties; what goes wrong, and how would you restructure it?

level: seniorimportance: should knowfreq 32%

answer

  1. else branch catches every other platform
  2. one select block per style
  3. shared outside, per-platform inside
  4. select replaces, never merges
  5. runtime values need hooks, not Platform

basics

~20 s

Scattered ternaries hide each platform's full style, give every non-iOS target the Android value, and invite drift. Group differences into one Platform.select per StyleSheet entry, with shared properties outside, an explicit default, and runtime-varying values kept out.

solid answer

~40 s

Per-property ternaries make it impossible to see what the header looks like on either platform, they send every non-iOS target down the Android branch, and adding a property means remembering yet another ternary. I would keep shared properties plain in `StyleSheet.create` and spread one `Platform.select({ ios, android, default })` per style entry, or pull the numbers into a small platform metrics object. `Platform.select` replaces rather than merges, so anything common stays outside it. Values that change while the app runs, such as color scheme or window size, do not belong in `Platform` branches at module scope; they come from hooks. And when the header's structure, not its values, differs, that is a sign to split components instead. Metro inlines static `Platform.select` calls, so the tidy version costs nothing at runtime.

code

tsx · 25 lines
tsx
import { Platform, StyleSheet } from 'react-native';

export const headerMetrics = Platform.select({
  ios: { height: 96, titleSize: 17 },
  android: { height: 56, titleSize: 20 },
  default: { height: 64, titleSize: 20 },
});

export const headerStyles = StyleSheet.create({
  container: {
    paddingHorizontal: 16,
    height: headerMetrics.height,
    ...Platform.select({
      ios: { justifyContent: 'flex-end' as const, paddingBottom: 8 },
      default: { justifyContent: 'center' as const },
    }),
  },
  title: {
    fontSize: headerMetrics.titleSize,
    ...Platform.select({
      ios: { textAlign: 'center' as const },
      default: { textAlign: 'left' as const },
    }),
  },
});

go deeper

for a junior

Recall that Platform.select can return a whole style fragment, so several platform differences can live in one place inside StyleSheet.create.

for a middle

Explain why a ternary's else branch catches every non-iOS target, and that select replaces values without merging, so shared properties stay outside it.

for a senior

Refactor for readability: one select per style entry, shared metrics, explicit defaults, runtime values through hooks, and recognize when structural differences need separate components.

for a principal

Set the convention for the codebase, where value-level platform differences live and when a component graduates to per-platform implementations, and enforce it in review.

## The smell A weather app's `ForecastHeader` starts with one platform difference: a taller header on iOS. Months later its styles look like this: `height: Platform.OS === 'ios' ? 96 : 56`, `paddingTop: Platform.OS === 'ios' ? 44 : 0`, `fontSize: Platform.OS === 'ios' ? 17 : 20`, `textAlign: Platform.OS === 'ios' ? 'center' : 'left'`, repeated across six style entries. Each line is correct on its own. Together they cause problems that interviewers want you to name. ## What goes wrong 1. **No platform is readable as a whole.** To know what the Android header looks like you must mentally evaluate every ternary. Reviewers miss mismatched values, such as an iOS title size paired with an Android line height. 2. **The else branch is not "Android".** `Platform.OS === 'ios' ? a : b` gives `b` to every platform that is not iOS. If the app also builds for another target, it silently inherits Android styling nobody chose for it. 3. **Drift and duplication.** The same literal, say `56`, appears in the header, the search bar offset and a list's content inset, and one of them gets updated alone. 4. **Branching in render.** When ternaries move into inline style objects inside the component, they are re-evaluated and allocate new objects on every render, and the platform logic is now mixed with state-driven styling. ## Restructuring - **One select per style entry.** Keep shared properties as plain keys in `StyleSheet.create` and spread a single `Platform.select({ ios: {...}, android: {...}, default: {...} })` for the differences. Each platform's values sit together and can be read at a glance. - **Name the default.** An explicit `default` (or `native` plus `default`) states what other targets get, instead of inheriting it from an else branch. - **Extract metrics.** When several components share the numbers, create a `headerMetrics` object with `Platform.select` once and import it, so `56` lives in one place. - **Mind the merge semantics.** `Platform.select` returns one value and replaces the others; it never deep-merges. Put properties needed everywhere outside the select, and remember that in an object spread, later keys override earlier ones. A safe order for the refactor: 1. list every ternary in the header's styles and group them by style entry; 2. move each group into one `Platform.select` spread, keeping shared properties outside; 3. add an explicit `default` and confirm with a screenshot on each platform that nothing moved; 4. extract values used by more than one component into a metrics object; 5. move anything that depends on runtime state into hooks and dynamic style entries. | Approach | Readable per platform | Default explicit | Good for | |---|---|---|---| | Ternary per property | no | no, else branch | one isolated value | | One `Platform.select` per style entry | yes | yes | several related values | | Shared metrics object | yes | yes | values reused across components | ## What does not belong in Platform branches `Platform` answers a question fixed for the life of the process: which operating system is this. Styles built with it at module scope in `StyleSheet.create` are evaluated once, which is exactly right. Values that change while the app runs are different: the color scheme, the window width after rotation or a foldable unfolding, the user's font scale. Branching those through `Platform` or freezing them into module-scope styles is a bug; they come from hooks and are applied as dynamic style entries. Similarly, if the iOS and Android headers differ in **structure**, for example a large collapsing title on one and a toolbar with an overflow menu on the other, no amount of style branching will stay readable. That is the point at which a team moves to separate per-platform components, a separate technique with its own tradeoffs. ## Cost is not the argument Refactoring is about clarity, not speed. Metro, React Native's bundler, replaces `Platform.OS` with a string literal and a `Platform.select` over a plain object literal with the matching value while transforming each file, and release builds fold the resulting constant conditions away. Ternaries and selects therefore both compile down to the chosen value, provided `Platform` is imported from `react-native` and the select object has no spreads or computed keys.

  • Why is it a bug to build a React Native header's colors with Platform.select inside StyleSheet.create when the app supports dark mode?
    `StyleSheet.create` at module scope runs once, and `Platform` never changes, so the select is frozen at startup. The color scheme can change while the app runs. Colors that follow the scheme must come from a hook such as `useColorScheme` or a theme context and be applied as a dynamic style entry, while static layout stays in the stylesheet.
  • In React Native, when does Metro fail to inline a Platform.select call?
    Metro only replaces calls it can resolve statically: `Platform` must be imported from `react-native`, and the argument must be an object literal with plain identifier or string keys, no spreads and no computed keys. A select over a variable or a spread object is left as a normal runtime call, which still works, just without stripping the unused branches.

saying these in an interview costs you the question

  • The else branch of a Platform.OS === 'ios' ternary only affects Android.
  • Platform.select merges the default object into the platform object.
  • Platform.select calls add measurable runtime cost, so ternaries are faster.
  • Dark mode colors can be chosen once with Platform.select at module scope.
  • Style branching can absorb any header difference, even a different component tree.