skip to content

A React Native recipe app should show one pane when a foldable phone is folded and two panes when unfolded; how would you build that switch?

level: seniorimportance: should knowfreq 34%

answer

  1. a fold is a window resize
  2. no hinge API in core
  3. breakpoint on window width in points
  4. lift state above the switch
  5. test split-screen and rotation too

basics

~20 s

Treat the fold as a window resize: read width from useWindowDimensions at the screen root, pick one or two panes at a width threshold in points, and keep the selected recipe in state above both layouts so switching panes does not lose it.

solid answer

~50 s

Core React Native exposes no hinge or posture API: unfolding reaches JavaScript as a **window size change**, the same `change` event a rotation or split-screen produces. So the screen root reads `width` from `useWindowDimensions()` and derives one decision, compact or expanded, from a threshold in points such as 600. Compact renders the recipe list and pushes the detail on selection; expanded renders list and detail side by side. The trap is state: the two layouts are different trees, so anything held inside a pane that unmounts, like the open recipe or a running step timer, resets on fold. Lift the selected recipe id, the current step and timers above the switch, or into a store. Base the decision on the window, never the screen or a device-model list, and test rotation, split-screen and tablets with the same code path.

code

tsx · 36 lines
tsx
import { useState } from 'react';
import { StyleSheet, View, useWindowDimensions } from 'react-native';
import { RecipeDetail, RecipeList } from './recipes';

const EXPANDED_MIN_WIDTH = 600;

export function RecipesScreen() {
  const { width } = useWindowDimensions();
  const expanded = width >= EXPANDED_MIN_WIDTH;
  const [selectedId, setSelectedId] = useState<string | null>(null);

  if (!expanded) {
    return selectedId ? (
      <RecipeDetail id={selectedId} onBack={() => setSelectedId(null)} />
    ) : (
      <RecipeList onSelect={setSelectedId} />
    );
  }

  return (
    <View style={styles.row}>
      <View style={styles.list}>
        <RecipeList onSelect={setSelectedId} />
      </View>
      <View style={styles.detail}>
        {selectedId ? <RecipeDetail id={selectedId} /> : null}
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  row: { flex: 1, flexDirection: 'row' },
  list: { flex: 1, borderRightWidth: 1 },
  detail: { flex: 2 },
});

go deeper

for a junior

Recall that unfolding a foldable arrives as a window size change that useWindowDimensions picks up.

for a middle

Explain the breakpoint in points, why window beats screen and device checks, and how compact and expanded layouts are rendered.

for a senior

Protect state across the layout switch, keep re-render cost down by reading the window once, and test folds, rotation and split-screen with one code path.

for a principal

Decide how far to invest in large-screen layouts, whether hinge-aware design justifies a native dependency, and which breakpoints the product standardises on.

## What a fold looks like to React Native When a foldable phone opens, the app's window becomes much wider. React Native does not surface the hinge itself: core has no API for the hinge's position or the device's posture. What it does deliver is the result, a **window dimensions change**, emitted as the same `change` event that rotation, split-screen and iPad multitasking produce. `useWindowDimensions()` re-renders with the new `width` and `height`. That is good news for design. A layout that responds to window size handles foldables, tablets, rotation and split-screen with **one code path**. If a design genuinely needs the hinge position, for example to avoid placing a button across the fold, that requires a native module or a community library, and is a separate decision. ## Deciding the layout from the window 1. At the screen's root, read `const { width } = useWindowDimensions()`. 2. Derive one boolean or enum: `expanded = width >= 600`. The threshold is in **points**, not pixels, so it means the same physical room on every density. 3. Render the compact layout (list, then detail on selection) or the expanded one (list and detail side by side). 4. Let flexbox size the panes inside the expanded layout, for example `flex: 1` for the list and `flex: 2` for the detail, so no pane width is computed by hand. | Situation | Window width | Layout | |---|---|---| | Folded phone, portrait | Narrow | One pane | | Unfolded foldable | Wide | Two panes | | Phone rotated to landscape | Often above the threshold | Two panes, if the design allows | | Tablet in split-screen at a third of the display | Narrow | One pane | | Tablet full screen | Wide | Two panes | ## Choosing the threshold The threshold is a product decision, not a platform constant. Work it out from content: - Find the narrowest width at which the recipe list is still usable, say about 280 points for a thumbnail and a title. - Find the narrowest comfortable detail pane, say about 360 points for ingredients and steps. - Add the gutter between them; the sum is the smallest window that can hold both. Round that to one named constant, such as `EXPANDED_MIN_WIDTH = 600`, and use it everywhere, so every screen agrees on when the app is "expanded". ## Window, not screen, not device model - `Dimensions.get('screen')` stays the same when the app is squeezed into split-screen, so a screen-based decision would show two panes in a narrow window. - `PixelRatio.get()` says nothing about available room; density and size are unrelated. - A list of known foldable models goes stale with every device release and misses split-screen entirely. ## Keeping state across the switch The compact and expanded layouts are **different component trees**. When the width crosses the threshold, React unmounts one and mounts the other, and any state held **inside** the unmounted pane is lost: the open recipe, the current step, a running kitchen timer, a half-typed note. - Keep the selected recipe id and the current step in state at the screen root, above the layout switch, or in a store. - Keep long-running work such as a timer outside the pane components entirely, so it keeps counting through the switch. - Accept or restore smaller losses deliberately: a list's scroll position resets when the list remounts, unless you save the offset and restore it. ## Controlling re-render cost `useWindowDimensions()` re-renders on any change to `width`, `height`, `scale` or `fontScale`. If every card in the recipe list called it, a single resize would re-render them all. - Read the window **once**, at the screen root. - Pass down the derived decision, not the raw numbers. - Memoize heavy children so they re-render only when that decision changes, not on every pixel of a resize. ## Testing the switch - Fold and unfold on a real device or a foldable emulator profile while a recipe is open and a timer is running. - Rotate a phone and a tablet with the same screen open. - Enter split-screen at several sizes and confirm the threshold behaves. - Check that deep links into a recipe land correctly in both layouts. A foldable is not a special platform to branch on; it is one more reason the window can change size at any moment.

  • How would you keep the recipe list's scroll position when the layout switches?
    The list remounts because it sits at a different place in the tree in each layout. Either save its scroll offset from `onScroll` into state held at the screen root and restore it on mount, or structure both layouts so the list stays at the same position in the tree and only the detail pane appears or disappears beside it.
  • When would you choose container size over window size for this decision?
    When the component does not own the whole window, for example a recipe card placed in a sidebar on tablets and full width on phones. Then the container's own size, measured with a `View`'s `onLayout`, is the right input, because the window may be wide while the card's slot is narrow.

saying these in an interview costs you the question

  • Core React Native provides a hinge or posture event for foldables.
  • The screen size is the right input, since the display is what folds.
  • Detect foldables by matching device model names.
  • Folding the phone restarts the JavaScript bundle, so state is lost anyway.
  • Each card should call useWindowDimensions to size itself.