Skip to content

Join the Seedly owners community →

React

Props & Data Flow

Learn how to pass data between components using props and understand React's one-way data flow

Written by 13 min read1 activity
Pixl, your presenter

Pixl presents

Data flows one way in React. Fight it and you get spaghetti. Pass props down, lift state up, done.Data flows one way in React. Fight it and you get spaghetti. Pass props down, lift state up, done.

Pixl pours water into the top of a tiered fountain that flows down into smaller basins
Data flows one way in React, from parent down to child

Your components are pretty good at handling their own state and events at this point. Real apps are built from LOTS of components working together, though. A parent component has to hand data down to its children, and those children need some way to talk back up. That's where props come in.

What Are Props?#

Props (short for "properties") are how a parent component passes data to a child component. They work a lot like function arguments. You pass values in and the child uses them.

// Parent passes data via props
function App() {
  return <UserCard name="Alice" age={28} isOnline={true} />;
}
 
// Child receives props as a parameter
function UserCard({ name, age, isOnline }) {
  return (
    <div className="card">
      <h2>{name}</h2>
      <p>Age: {age}</p>
      <span>{isOnline ? 'Online' : 'Offline'}</span>
    </div>
  );
}

The parent decides what data to pass. The child decides how to show it. Keeping those two jobs separate is what makes components reusable, so the same UserCard works for any user you throw at it.

Props vs. State#

These two trip up a lot of beginners, but they've got different jobs.

FeaturePropsState
Who controls it?Parent componentThe component itself
Can the component change it?No (read-only)Yes (with setter function)
PurposeConfigure a child from outsideTrack changing data inside
Triggers re-render?Yes, when parent passes new valuesYes, when setter is called

Destructuring Props#

You'll almost always destructure props right in the function parameter. It's a lot cleaner than typing props.name, props.age and so on over and over.

// Destructured (preferred):
function Greeting({ name, emoji }) {
  return <p>{emoji} Hello, {name}!</p>;
}
 
// Without destructuring (works but verbose):
function Greeting(props) {
  return <p>{props.emoji} Hello, {props.name}!</p>;
}

Default Prop Values#

You can give props default values using plain old JavaScript default parameters.

function Button({ label = 'Click me', variant = 'primary', size = 'medium' }) {
  return (
    <button className={`btn btn-${variant} btn-${size}`}>
      {label}
    </button>
  );
}
 
// All of these work:
<Button />                           // Uses all defaults
<Button label="Save" />              // Custom label, default variant and size
<Button variant="danger" size="lg" /> // Custom variant and size, default label

The Children Prop#

There's one special prop called children, and it holds whatever you put between a component's opening and closing tags.

function Card({ children, title }) {
  return (
    <div className="card">
      <h3>{title}</h3>
      <div className="card-body">
        {children}
      </div>
    </div>
  );
}
 
// Usage - anything between the tags becomes children
function App() {
  return (
    <Card title="My Card">
      <p>This paragraph is the children prop.</p>
      <button>So is this button.</button>
    </Card>
  );
}

The children prop is what makes wrapper components possible. Those are components that handle styling or layout without needing to know (or care) what's inside them.

One-Way Data Flow#

Pixl pulls a cord at the foot of a staircase that rings a brass bell at the top
Children talk back up by calling a function the parent passed down

React enforces one-way data flow, which means data flows down from parent to child through props. That keeps your app predictable, because you always know where data came from... just look at the parent.

Step 1. Data Flows Down#

Parents pass data to children through props. The parent is the "source of truth" for that data.

Step 2. Children Can't Send Data Up Directly#

A child can't reach up and change its parent's state. It's a one-way street (and honestly that's a good thing).

Step 3. Callback Props Enable Upward Communication#

To talk upward, parents pass functions down as props. Children call those functions with some data, and the parent's function updates the parent's state.

Passing Functions as Props (Lifting State Up)#

When a child needs to update something in the parent, the parent hands it a callback function.

import { useState } from 'react';
 
function Parent() {
  const [selectedItem, setSelectedItem] = useState(null);
 
  return (
    <div>
      <h2>Selected: {selectedItem || 'None'}</h2>
      <ItemList onSelect={setSelectedItem} />
    </div>
  );
}
 
function ItemList({ onSelect }) {
  const items = ['React', 'Vue', 'Angular'];
 
  return (
    <ul>
      {items.map(item => (
        <li key={item}>
          <button onClick={() => onSelect(item)}>
            {item}
          </button>
        </li>
      ))}
    </ul>
  );
}

When somebody clicks "React" in the child, it calls onSelect('React'). That's secretly the parent's setSelectedItem wearing a different name tag. The parent's state updates and the new selection shows up on screen.

A Real-World Example: Todo App#

Here's props and data flow working together in a mini todo app.

function TodoApp() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React', done: false },
    { id: 2, text: 'Build an app', done: false },
  ]);
 
  function toggleTodo(id) {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    ));
  }
 
  return (
    <div>
      <h1>My Todos</h1>
      <TodoList todos={todos} onToggle={toggleTodo} />
    </div>
  );
}
 
function TodoList({ todos, onToggle }) {
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem key={todo.id} todo={todo} onToggle={onToggle} />
      ))}
    </ul>
  );
}
 
function TodoItem({ todo, onToggle }) {
  return (
    <li
      onClick={() => onToggle(todo.id)}
      style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
    >
      {todo.text}
    </li>
  );
}

Data flows down (the todos prop goes from TodoApp to TodoList to TodoItem) and actions flow up (clicking an item calls onToggle, which reaches all the way back up to TodoApp to update state). Read that one twice if you need to. It's THE pattern.

TL;DR#

  • Props pass data from parent to child (kinda like function arguments)
  • Props are read-only, so children can't change them
  • Use destructuring and default values to keep prop handling clean
  • The children prop lets a component wrap whatever content you give it
  • React uses one-way data flow, so data flows down through props
  • To talk upward, pass callback functions down as props
  • Lift state up to the closest shared parent when siblings need the same data

What's Next?#

You can build components that talk to each other through props and callbacks now. So what about stuff that happens outside your component, like fetching data from an API, setting timers or updating the page title? Next lesson covers useEffect and component lifecycle, which is how React deals with side effects...

This lesson ends with a short activity.