0Pricing
React Native Academy · Lesson

Passing Data with Props

Define a reusable component that accepts props, pass values from a parent component, and use PropTypes or TypeScript to document expected prop shapes.

Passing Data with Props is a free React Native Academy lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the React Native Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

What Are Props?

Props (short for properties) are the mechanism React components use to receive input from their parent. A parent component renders a child component and passes data to it through attributes in JSX — just like HTML attributes, but for any JavaScript value including strings, numbers, objects, arrays, and functions. The child component receives all its props as a single object argument. Props flow in one direction: from parent down to child. A child component can never modify its own props — they are read-only.

import { Text, View } from 'react-native';

// Child component receives props
function Greeting({ name, age }) {
  return (
    <View>
      <Text>Hello, {name}!</Text>
      <Text>You are {age} years old.</Text>
    </View>
  );
}

// Parent passes data through props
export default function App() {
  return (
    <View style={{ flex: 1, padding: 24 }}>
      <Greeting name='Alice' age={30} />
      <Greeting name='Bob' age={25} />
    </View>
  );
}

Destructuring Props

You can access props in two ways: as a single props object (function Card(props) { props.title }) or by destructuring directly in the function signature (function Card({ title, subtitle })). Destructuring is the preferred style because it makes the accepted props explicit and the code more concise. You can also provide default values for props in the destructuring syntax: { size = 16, color = '#333' }. This means the prop is optional and falls back to the default when not provided by the parent.

import { Text, View, StyleSheet } from 'react-native';

// Destructuring with default values
function Badge({ label, color = '#4f86f7', size = 14 }) {
  return (
    <View style={[styles.badge, { backgroundColor: color }]}>
      <Text style={[styles.text, { fontSize: size }]}>{label}</Text>
    </View>
  );
}

export default function App() {
  return (
    <View style={{ gap: 8, padding: 16 }}>
      <Badge label='New' />                        {/* uses defaults */}
      <Badge label='Sale' color='#e74c3c' />       {/* custom color */}
      <Badge label='Pro' color='#2ecc71' size={12} />
    </View>
  );
}

const styles = StyleSheet.create({
  badge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 12, alignSelf: 'flex-start' },
  text: { color: '#fff', fontWeight: 'bold' },
});

Passing Different Prop Types

Props can carry any JavaScript value. Strings don't need curly braces (title='Hello'), but all other types do — numbers (count={5}), booleans (disabled={true} or just disabled), objects (style={{ color: 'red' }}), arrays (items={[1,2,3]}), and functions (onPress={handlePress}). Passing functions as props is how children communicate back to parents — the parent defines the function and passes it down; the child calls it when something happens. This pattern is central to React's unidirectional data flow.

import { View, Text, TouchableOpacity } from 'react-native';

// onPress is a function prop — callback from child to parent
function ConfirmButton({ label, count, disabled, onPress }) {
  return (
    <TouchableOpacity
      onPress={onPress}
      disabled={disabled}
      style={{ opacity: disabled ? 0.4 : 1, backgroundColor: '#4f86f7', padding: 12, borderRadius: 8 }}
    >
      <Text style={{ color: '#fff', fontWeight: 'bold' }}>
        {label} ({count})
      </Text>
    </TouchableOpacity>
  );
}

export default function App() {
  return (
    <View style={{ padding: 24 }}>
      <ConfirmButton
        label='Add to Cart'
        count={3}
        disabled={false}
        onPress={() => console.log('Added!')}
      />
    </View>
  );
}

The children Prop

The special children prop holds whatever you put between the opening and closing tags of a component. This enables composition — you can build wrapper or container components that apply layout, styling, or behavior to any content nested inside. React Native's View, ScrollView, and TouchableOpacity all work this way. Define your own wrapper components with children to create reusable layout shells like cards, modals, and screen templates.

import { View, StyleSheet } from 'react-native';

// A Card wrapper that applies consistent styling
function Card({ children, style }) {
  return (
    <View style={[styles.card, style]}>
      {children}
    </View>
  );
}

export default function App() {
  return (
    <View style={{ padding: 16 }}>
      <Card>
        {/* Any content can go inside the Card */}
        <Text>Title</Text>
        <Text>Subtitle</Text>
        <Image source={{ uri: 'https://picsum.photos/200' }} style={{ width: 200, height: 100 }} />
      </Card>
    </View>
  );
}

const styles = StyleSheet.create({
  card: { backgroundColor: '#fff', borderRadius: 16, padding: 16, elevation: 3, marginBottom: 12 },
});

Prop Spreading with the Spread Operator

Use the JavaScript spread operator to pass all properties of an object as individual props. This is convenient when you have a data object with many fields: <UserCard {...user} /> is equivalent to <UserCard id={user.id} name={user.name} avatar={user.avatar} />. The spread pattern is common when rendering items from an array with .map(). Be careful not to spread unknown props onto native components (View, Text) — they will trigger warnings or be silently ignored. Only spread props onto custom components that you control.

import { View, Text } from 'react-native';

const users = [
  { id: '1', name: 'Alice', role: 'Admin' },
  { id: '2', name: 'Bob', role: 'User' },
];

function UserRow({ name, role }) {
  return (
    <View style={{ padding: 12, borderBottomWidth: 1, borderBottomColor: '#eee' }}>
      <Text style={{ fontWeight: 'bold' }}>{name}</Text>
      <Text style={{ color: '#666' }}>{role}</Text>
    </View>
  );
}

export default function UserList() {
  return (
    <View>
      {users.map(user => (
        <UserRow key={user.id} {...user} />
      ))}
    </View>
  );
}

TypeScript Props with Interfaces

In TypeScript, define the shape of your props using an interface or type. This catches errors at compile time — passing a number where a string is expected, or forgetting a required prop, shows an error before you even run the app. Mark optional props with ?. TypeScript also enables editor auto-complete: when you type <MyComponent in JSX, your editor lists all available props and their types. This is one of the biggest productivity benefits of using TypeScript in React Native projects.

import { View, Text, StyleSheet } from 'react-native';

interface ProductCardProps {
  id: string;
  name: string;
  price: number;
  imageUri: string;
  badge?: string;      // optional
  onPress?: () => void; // optional callback
}

export default function ProductCard({
  name,
  price,
  imageUri,
  badge,
  onPress,
}: ProductCardProps) {
  return (
    <View style={styles.card}>
      <Text style={styles.name}>{name}</Text>
      <Text style={styles.price}>${price.toFixed(2)}</Text>
      {badge && <Text style={styles.badge}>{badge}</Text>}
    </View>
  );
}

const styles = StyleSheet.create({
  card: { padding: 16, backgroundColor: '#fff', borderRadius: 12 },
  name: { fontSize: 16, fontWeight: '600' },
  price: { fontSize: 18, color: '#4f86f7', fontWeight: 'bold', marginTop: 4 },
  badge: { color: '#e74c3c', fontSize: 12, marginTop: 4 },
});

Callback Props: Child to Parent Communication

Since data flows only downward in React, a child communicates back to its parent by calling a callback function passed as a prop. The parent defines the function (what should happen when the user acts), passes it to the child, and the child calls it at the right time. This is the standard pattern for buttons, form inputs, and list items — the child component doesn't care about what happens next (no business logic), it just calls the prop function with any relevant data.

import { View, Text, TouchableOpacity } from 'react-native';

// Child: knows nothing about business logic
function DeleteButton({ itemId, onDelete }) {
  return (
    <TouchableOpacity onPress={() => onDelete(itemId)}>
      <Text style={{ color: 'red' }}>Delete</Text>
    </TouchableOpacity>
  );
}

// Parent: owns the logic and state
export default function ItemList() {
  const [items, setItems] = React.useState(['Apple', 'Banana', 'Cherry']);

  function handleDelete(index) {
    setItems(prev => prev.filter((_, i) => i !== index));
  }

  return (
    <View>
      {items.map((item, i) => (
        <View key={i} style={{ flexDirection: 'row', justifyContent: 'space-between', padding: 12 }}>
          <Text>{item}</Text>
          <DeleteButton itemId={i} onDelete={handleDelete} />
        </View>
      ))}
    </View>
  );
}

Avoiding Prop Drilling

Prop drilling happens when you pass props through several intermediate components that don't need the data themselves — they just forward it deeper. For example, passing a user object from App → Screen → Section → Card → Avatar just to display the avatar image. This becomes difficult to maintain as the hierarchy grows. The solution is to use Context (for global or shared data like the logged-in user or theme) or to colocate state closer to the components that need it. Don't over-engineer early — a few levels of prop passing is perfectly fine.

// Prop drilling anti-pattern (not ideal when deep)
function App() {
  const user = { name: 'Alice', avatar: 'https://...' };
  return <Screen user={user} />;
}
function Screen({ user }) {
  return <Section user={user} />; // just passing through
}
function Section({ user }) {
  return <Avatar user={user} />; // just passing through
}
function Avatar({ user }) {
  return <Image source={{ uri: user.avatar }} style={{ width: 40, height: 40 }} />; // actually uses it
}

// Solution: Context (covered later)
// Or: pass only the specific value needed at each level
function Section({ avatarUri }) { ... }
function Avatar({ uri }) { ... }

Immutable Props: Why Children Cannot Modify Them

A core rule of React: a component must never modify its own props. Props are the parent's data passed to the child for display — they are owned by the parent. If a child needs to change a value, it should call a callback prop to tell the parent, and the parent updates its state. The parent then passes the updated value back down as a new prop. This unidirectional flow makes data changes predictable and debuggable — you always know where state lives and who controls it.

// WRONG: Never mutate props directly
function Counter({ count }) {
  // This is an error — do not do this:
  // count = count + 1;

  return <Text>{count}</Text>;
}

// CORRECT: Notify parent via callback, parent updates state
function Counter({ count, onIncrement }) {
  return (
    <TouchableOpacity onPress={onIncrement}>
      <Text>{count}</Text>
    </TouchableOpacity>
  );
}

// In the parent:
const [count, setCount] = useState(0);
<Counter count={count} onIncrement={() => setCount(c => c + 1)} />

Real-World Props Example: Feed Post

Apply all prop concepts to a realistic social media feed post component. The component receives data from the feed list (text, image, author, like count, timestamp) and callback functions for user interactions (like, comment, share). The component is purely presentational — all data comes in through props, all interactions flow out through callback props. This design makes the FeedPost component easy to test in isolation and reuse across different feed types (home feed, profile feed, search results).

import { View, Text, Image, TouchableOpacity, StyleSheet } from 'react-native';

export default function FeedPost({
  author, avatarUri, imageUri, caption, likes, timeAgo,
  onLike, onComment, onShare
}) {
  return (
    <View style={styles.post}>
      <View style={styles.header}>
        <Image source={{ uri: avatarUri }} style={styles.avatar} />
        <View>
          <Text style={styles.author}>{author}</Text>
          <Text style={styles.time}>{timeAgo}</Text>
        </View>
      </View>
      <Image source={{ uri: imageUri }} style={styles.postImage} />
      <Text style={styles.caption}>{caption}</Text>
      <View style={styles.actions}>
        <TouchableOpacity onPress={onLike}><Text>❤️ {likes}</Text></TouchableOpacity>
        <TouchableOpacity onPress={onComment}><Text>💬 Comment</Text></TouchableOpacity>
        <TouchableOpacity onPress={onShare}><Text>🔗 Share</Text></TouchableOpacity>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  post: { backgroundColor: '#fff', marginBottom: 8 },
  header: { flexDirection: 'row', alignItems: 'center', padding: 12, gap: 10 },
  avatar: { width: 40, height: 40, borderRadius: 20 },
  author: { fontWeight: 'bold', fontSize: 14 },
  time: { fontSize: 12, color: '#999' },
  postImage: { width: '100%', aspectRatio: 1 },
  caption: { padding: 12, fontSize: 14 },
  actions: { flexDirection: 'row', gap: 20, padding: 12, borderTopWidth: 1, borderTopColor: '#eee' },
});

Default Props with defaultProps

In addition to destructuring defaults, React components can define defaultProps as a static property — a plain object that specifies fallback values for each optional prop. When a prop is undefined (not passed by the parent), React substitutes the value from defaultProps. This pattern is older than destructuring defaults and works the same way, but is less common in modern TypeScript codebases where interface defaults are preferred. Both approaches achieve the same result: making optional props safe to consume without null checks.

import { Text, View, StyleSheet } from 'react-native';

function Avatar({ size, color, initials }) {
  return (
    <View style={[styles.circle, { width: size, height: size, borderRadius: size / 2, backgroundColor: color }]}>
      <Text style={styles.initials}>{initials}</Text>
    </View>
  );
}

// defaultProps: applied when prop is undefined
Avatar.defaultProps = {
  size: 48,
  color: '#4f86f7',
};

// Usage:
<Avatar initials='AB' />                    // uses defaults: 48px blue
<Avatar initials='CD' size={64} />          // custom size, default color
<Avatar initials='EF' size={32} color='#e74c3c' />  // all custom

const styles = StyleSheet.create({
  circle: { alignItems: 'center', justifyContent: 'center' },
  initials: { color: '#fff', fontWeight: 'bold', fontSize: 16 },
});

Quick Check

Test your understanding of React Native Mobile Development concepts from this lesson.

Lesson Recap

In this lesson you learned: props are read-only values passed from parent to child in one direction, callback function props allow children to notify parents of user actions, and TypeScript interfaces document and enforce the expected prop shape at compile time. Next up we explore managing changing values inside components with the useState hook.

Frequently asked questions

Is the “Passing Data with Props” lesson free?

Yes — the full text of “Passing Data with Props” is free to read here on the web, and the React Native Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the React Native Academy course, upgrade to CoddyKit PRO.

What will I learn in “Passing Data with Props”?

Define a reusable component that accepts props, pass values from a parent component, and use PropTypes or TypeScript to document expected prop shapes. You practise React Native Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start React Native Academy?

No prior experience is required. React Native Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Passing Data with Props” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this React Native Academy lesson?

Yes. Every React Native Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Passing Data with Props
  2. Managing Component State with useState
  3. Lifting State Up
  4. Building an Interactive Counter App
← Back to React Native Academy