Props

How data gets into a component: the props object, destructuring with defaults, children, and a type that names what it takes.

Syntaxfunction Avatar({ uri, size = 40 }: AvatarProps) { /* ... */ }

Props are a component's inputs. Without them a component renders the same thing forever. React calls your function with exactly one argument: an object holding everything the caller passed as an attribute. <Avatar size={40} /> arrives as { size: 40 }.

Destructuring and defaults

The longhand is function Avatar(props) { const size = props.size; }. The shorthand is to destructure in the parameter list: function Avatar({ size }). They are the same thing. Adding = 40 gives a default, used when the prop is undefined — which includes the caller leaving it out, but not the caller passing null or 0.

children

Whatever sits between the opening and closing tags arrives as the children prop. <Card><Text>Hi</Text></Card> is shorthand for <Card children={<Text>Hi</Text>} />. Both work; the nested form is the one people read.

One direction only

A component never writes to its props. Data flows from parent to child, and a child that assigns to a prop changes nothing on screen. Values that change over time are state, which comes later.

What trips people up

Passing an object or an array needs two sets of braces, as the outer pair is the expression. Spreading, <Row {...row} />, forwards every key in the object, including ones you did not intend. The examples declare a type above each component listing the props it accepts, so a misspelled prop is caught before the app runs; the TypeScript stage covers that syntax in full.

Example

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

// The props this component accepts. The question mark marks unit optional.
type StatRowProps = {
  label: string;
  value: number;
  unit?: string;
};

// Destructuring in the parameter list is the short form of
// function StatRow(props) { const label = props.label; ... }
// The = "kg" default applies only when the caller omits unit entirely.
function StatRow({ label, value, unit = "kg" }: StatRowProps) {
  return (
    <View style={styles.row}>
      <Text style={styles.label}>{label}</Text>
      <Text style={styles.value}>{value} {unit}</Text>
    </View>
  );
}

export default function App() {
  return (
    <View style={styles.screen}>
      <StatRow label="Carry-on" value={7} />
      <StatRow label="Checked" value={23} />
      {/* A string prop takes quotes; anything else takes braces. */}
      <StatRow label="Width" value={55} unit="cm" />
    </View>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1, paddingTop: 80, paddingHorizontal: 24 },
  // flexDirection must be set to put children side by side:
  // the default here is column, unlike CSS.
  row: { flexDirection: "row", justifyContent: "space-between", paddingVertical: 10 },
  label: { fontSize: 16, color: "#333333" },
  value: { fontSize: 16, fontWeight: "600" },
});

// On screen: three rows, each with its label on the left and its value on the
// right. The first two read "7 kg" and "23 kg"; the third reads "55 cm".

When to use it

  • Make one Row component serve every row of a settings screen
  • Give a component defaults so the common caller passes almost nothing
  • Wrap whatever content a caller supplies in a shared Card using children
  • Forward a prepared object of props to a component with a spread

More examples

children, and the prop it is shorthand for

Two bordered cards, "Gate" over "B14" and "Seat" over "14A", rendered by the same component from the two different syntaxes. Use children whenever the wrapper should not care what it contains. Seeing the longhand once makes clear that children is not special magic, just a prop with a reserved name.

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

type CardProps = {
  title: string;
  // ReactNode covers anything renderable: elements, strings, numbers, null.
  children: ReactNode;
};

function Card({ title, children }: CardProps) {
  return (
    <View style={styles.card}>
      <Text style={styles.title}>{title}</Text>
      {children}
    </View>
  );
}

export default function App() {
  return (
    <View style={styles.screen}>
      {/* Nested form. Identical to the explicit prop below. */}
      <Card title="Gate">
        <Text>B14</Text>
      </Card>

      {/* Longhand: children passed as an ordinary prop. */}
      <Card title="Seat" children={<Text>14A</Text>} />
    </View>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1, paddingTop: 80, paddingHorizontal: 24, gap: 12 },
  card: { borderWidth: 1, borderColor: "#DDDDDD", borderRadius: 10, padding: 14 },
  title: { fontSize: 12, color: "#666666", marginBottom: 4 },
});

Optional, boolean and spread props

Three short labels: "Delayed" grey, "On time" blue and bold, "Boarding" grey. It collects the three shorthands you will meet in other people's code, each next to the long form it stands for. The spread is convenient when props arrive as an object already, but it hides what is being passed, so prefer naming props explicitly on components you are still changing.

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

type TagProps = {
  text: string;
  muted?: boolean;
};

// Default the boolean: an omitted prop is undefined, not false.
function Tag({ text, muted = false }: TagProps) {
  return <Text style={[styles.tag, muted && styles.muted]}>{text}</Text>;
}

const delayed: TagProps = { text: "Delayed", muted: true };

export default function App() {
  return (
    <View style={styles.screen}>
      {/* Spread: the same as text={delayed.text} muted={delayed.muted} */}
      <Tag {...delayed} />

      {/* muted omitted, so the default false applies */}
      <Tag text="On time" />

      {/* Bare prop name. Shorthand for muted={true} */}
      <Tag text="Boarding" muted />
    </View>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1, paddingTop: 80, alignItems: "flex-start", paddingHorizontal: 24, gap: 6 },
  tag: { fontSize: 14, fontWeight: "600", color: "#0A7EA4" },
  muted: { color: "#999999", fontWeight: "400" },
});

Computing a prop before passing it

A pure function, so a console comment is the right way to show what it does. Pull logic like pluralisation out of the component and pass the finished string in as a prop: the component stays about layout, and the rule can be unit tested without rendering anything. This is the pattern the testing stage builds on.

Example · tsx
// lib/labels.ts
// Props are ordinary values, so they can be computed by ordinary functions.
// Keeping this out of the component makes it testable on its own.
export function labelFor(count: number, noun: string): string {
  if (count === 0) return `No ${noun}s`;
  return count === 1 ? `1 ${noun}` : `${count} ${noun}s`;
}

console.log(labelFor(0, "bag"));
// Output: No bags
console.log(labelFor(1, "bag"));
// Output: 1 bag
console.log(labelFor(3, "bag"));
// Output: 3 bags

// At the call site the result is just another string prop:
// <StatRow label={labelFor(bags.length, "bag")} value={totalWeight} />

Discussion

  • Be the first to comment on this lesson.