Composition

Fragments group siblings without adding a native view, and JSX passed as a prop turns repeated layout into a reusable wrapper.

Syntax<>{children}</> // shorthand for <Fragment>{children}</Fragment>

Two problems, one answer. A component must return a single element, and in React Native every element you add is a real native view that also becomes a flex child of its parent. Fragments solve the first without paying for the second.

Fragments

<>...</> groups children and renders nothing itself. It is shorthand for <Fragment>...</Fragment>, imported from react; the two are the same thing, except that the shorthand accepts no props. That matters in exactly one place: a fragment inside a map needs a key, so it needs the long form, <Fragment key={id}>.

JSX as a prop

Elements are values, so they can be passed like any other prop. children takes the body; a named prop such as header or footer takes a slot. A Card that accepts children works for any content, while a Card that accepts a body string only ever works for text.

Wrappers instead of repetition

There is no cascade here. A style on a parent view does not reach its children at all; the one exception is text styles inside nested <Text>. So shared padding, background or font cannot be declared once at the root the way CSS does it. The replacement is a component: a Screen that owns the screen padding, a Card that owns the border and spacing, used everywhere.

What trips people up

Replacing a <View> with a fragment removes a flex container, so its children join the grandparent's layout and the spacing changes.

Example

Example · tsx
// App.tsx
import type { ReactNode } from "react";
import { StyleSheet, Text, View } from "react-native";

type CardProps = {
  title: string;
  children: ReactNode;
  footer?: ReactNode; // a slot: JSX passed in as an ordinary prop
};

function Card({ title, children, footer }: CardProps) {
  return (
    <View style={styles.card}>
      <Text style={styles.title}>{title}</Text>
      {children}
      {/* Render the slot only when the caller supplied one. */}
      {footer ? <View style={styles.footer}>{footer}</View> : null}
    </View>
  );
}

// Screen owns the padding every screen would otherwise repeat. With no
// cascade in React Native, a wrapper component is how layout gets shared.
function Screen({ children }: { children: ReactNode }) {
  return <View style={styles.screen}>{children}</View>;
}

export default function App() {
  return (
    <Screen>
      <Card title="Flight BA432" footer={<Text style={styles.small}>Gate B14</Text>}>
        {/* A fragment groups two siblings without adding a native view. */}
        <>
          <Text>Departs 18:40</Text>
          <Text>Seat 14A</Text>
        </>
      </Card>
    </Screen>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1, paddingTop: 80, paddingHorizontal: 24 },
  card: { borderWidth: 1, borderColor: "#DDDDDD", borderRadius: 10, padding: 14 },
  title: { fontSize: 12, color: "#666666", marginBottom: 6 },
  footer: { borderTopWidth: 1, borderTopColor: "#EEEEEE", marginTop: 10, paddingTop: 8 },
  small: { fontSize: 12, color: "#666666" },
});

// On screen: one bordered card inset from both edges, headed "Flight BA432",
// with "Departs 18:40" and "Seat 14A" stacked inside it and "Gate B14" below
// a thin dividing line at the bottom of the card.

When to use it

  • Build one Card used on every screen with a title, a body and an optional footer
  • Return two sibling elements from a component without adding a wrapper view
  • Give every screen the same padding and background without repeating the style object
  • Render pairs of elements in a list without wrapping each pair in its own view

More examples

A keyed fragment inside a map

Four lines alternating bold term and grey definition. Because the fragment adds no view, both Text elements are direct children of the screen's View and lay out in its column, which is the point: a wrapper view here would have made each pair its own flex child with its own box. This is the only situation where you must write Fragment out in full.

Example · tsx
import { Fragment } from "react";
import { StyleSheet, Text, View } from "react-native";

type Entry = { id: string; term: string; definition: string };

const entries: Entry[] = [
  { id: "e1", term: "View", definition: "A container that maps to a native view" },
  { id: "e2", term: "Text", definition: "The only place a string may live" },
];

export default function Glossary() {
  return (
    <View style={styles.screen}>
      {entries.map((entry) => (
        // <>...</> cannot take a key, so the long form is required here.
        // Apart from props, <Fragment> and <> are the same thing.
        <Fragment key={entry.id}>
          <Text style={styles.term}>{entry.term}</Text>
          <Text style={styles.definition}>{entry.definition}</Text>
        </Fragment>
      ))}
    </View>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1, paddingTop: 80, paddingHorizontal: 24 },
  term: { fontSize: 16, fontWeight: "700", marginTop: 12 },
  definition: { fontSize: 14, color: "#555555" },
});

Named slots, so the caller decides the content

Two settings rows: the first with an @ on the left, "Email" filling the middle and a grey chevron on the right; the second just "About". Named slots are the right shape when a component controls layout but must not dictate content, which is most list rows, headers and dialogs. Note the chevron character sits in braces as a string, since a bare > in JSX would be read as markup.

Example · tsx
import type { ReactNode } from "react";
import { StyleSheet, Text, View } from "react-native";

type RowProps = {
  left?: ReactNode;
  right?: ReactNode;
  children: ReactNode;
};

// Row decides where things sit; the caller decides what they are.
// flexDirection must be set explicitly: the default is column, not row.
function Row({ left, right, children }: RowProps) {
  return (
    <View style={styles.row}>
      {left}
      <View style={styles.middle}>{children}</View>
      {right}
    </View>
  );
}

export default function Settings() {
  return (
    <View style={styles.screen}>
      <Row left={<Text>@</Text>} right={<Text style={styles.chevron}>{">"}</Text>}>
        <Text style={styles.label}>Email</Text>
      </Row>
      {/* Slots are optional, so the same Row works with nothing beside it. */}
      <Row>
        <Text style={styles.label}>About</Text>
      </Row>
    </View>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1, paddingTop: 80, paddingHorizontal: 24 },
  row: { flexDirection: "row", alignItems: "center", gap: 10, paddingVertical: 12 },
  middle: { flex: 1 }, // takes the space the two slots do not
  label: { fontSize: 16 },
  chevron: { fontSize: 16, color: "#999999" },
});

A text wrapper, because only text styles inherit

"Total" in the app's body style, then "48.20" in the same style but bold. The example exists to mark the single exception to there being no cascade: nested Text inherits from the Text above it, and nothing else inherits from anything. Build an AppText early in a project, because retrofitting a font onto hundreds of bare Text elements later is a long afternoon.

Example · tsx
import type { ReactNode } from "react";
import { StyleSheet, Text, View } from "react-native";

// Style inheritance in React Native exists only inside a text subtree, so an
// app-wide font cannot be set once at the root. A wrapper is the substitute.
function AppText({ children }: { children: ReactNode }) {
  return <Text style={styles.appText}>{children}</Text>;
}

export default function Receipt() {
  return (
    <View style={styles.screen}>
      <AppText>Total</AppText>

      {/* A nested <Text> does inherit from the <Text> around it: this line
          keeps the colour and size above and adds bold. That inheritance
          works nowhere else in the tree. */}
      <AppText>
        <Text style={styles.bold}>48.20</Text>
      </AppText>
    </View>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1, paddingTop: 80, paddingHorizontal: 24 },
  appText: { fontSize: 16, color: "#222222" },
  bold: { fontWeight: "700" },
});

Discussion

  • Be the first to comment on this lesson.