π¬ Props and Data Flow
Components are only useful when you can feed them data. Props are how a parent hands information to a child β and how React keeps data moving in one predictable direction. This lesson covers passing props, one-way flow, lifting state up, talking back to parents with callbacks, and escaping prop drilling with Context.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Pass props of any JavaScript type from a parent to a child component
- Explain React's unidirectional (one-way) data flow and why props are read-only
- Send data back up by passing callback functions as props
- Apply the lifting state up pattern to share state between sibling components
- Recognize prop drilling and use the Context API to avoid it
Estimated Time: 35β45 minutes β’ Difficulty: Intermediate
Hands-on: Build a temperature converter whose two inputs share lifted state.
In This Lesson
Props: The Communication Channel
Props (short for "properties") are how a parent component passes data to a child. You write them like HTML attributes on the child, and the child receives them as its function argument. Props are the primary way components communicate.
π‘ Analogy β the mail system. A parent component is the post office; props are the sealed packages it sends; child components are the houses that receive them. Mail flows in one direction (office β houses), and a house doesn't get to reach back and rewrite what's inside the package. That "sealed, one-way" nature is exactly how React props behave.
Passing and Reading Props
The parent sets props like attributes. Strings use quotes; anything else goes in curly braces (an expression):
function App() {
return (
<UserProfile
name="John Doe"
role="Developer"
isActive={true}
loginCount={42}
/>
);
}
The child reads them from its argument. Destructuring in the parameter list is the idiomatic way β it names exactly what the component needs:
function UserProfile({ name, role, isActive, loginCount }) {
return (
<div className="user-profile">
<h2>{name}</h2>
<p>Role: {role}</p>
<p>Status: {isActive ? 'Active' : 'Inactive'}</p>
<p>Logins: {loginCount}</p>
</div>
);
}
Default values
Give props fallbacks with standard default parameters so the component still renders when a prop is omitted:
function UserProfile({
name = 'Guest User',
role = 'Visitor',
isActive = false,
loginCount = 0,
}) {
/* β¦ */
}
π The special children prop
Whatever you put between a component's opening and closing tags arrives as a prop called children. It's the foundation of reusable wrappers:
function Card({ title, children }) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
</div>
);
}
// Usage β the <p> and <button> become `children`
<Card title="Welcome">
<p>This is the card content.</p>
<button>Learn More</button>
</Card>
Props Can Be Any Type
A prop can be any valid JavaScript value β not just strings. Strings use quotes; everything else uses { }:
| Type | Example |
|---|---|
| String | title="Hello" |
| Number | count={42} |
| Boolean | disabled={true} (or just disabled) |
| Object | user={{ id: 1, name: 'Ada' }} |
| Array | items={['a', 'b', 'c']} |
| Function | onClick={handleClick} |
| JSX element | icon={<SearchIcon />} |
Spreading a props object
If you already have an object of props, the spread operator forwards them all at once:
const buttonProps = { type: 'submit', className: 'primary', disabled: false };
<Button {...buttonProps} />
// identical to:
<Button type="submit" className="primary" disabled={false} />
π‘ A note on type checking
To document and validate the props a component expects, most modern projects use TypeScript, which checks types at build time:
interface UserProfileProps {
name: string;
age?: number; // optional
isAdmin?: boolean;
}
function UserProfile({ name, age = 21, isAdmin = false }: UserProfileProps) {
/* β¦ */
}
The older prop-types package does runtime checks and still appears in JavaScript-only codebases, but new projects overwhelmingly reach for TypeScript.
One-Way Data Flow
React enforces unidirectional data flow: data passes down the tree from parent to child through props, never sideways or upward on its own. This single rule is what makes React apps predictable β when a value is wrong on screen, you always know to look up the tree for its source.
π‘ Analogy β a waterfall. Water flows from higher pools to lower ones and never runs back uphill by itself. If a lower pool is muddy, the contamination came from above. React data behaves the same way, which is why tracing a bug is so much easier than in a system where data sloshes in every direction.
Two rules follow directly from one-way flow:
- Props are read-only. A component must never modify the props it receives. Treat them as a sealed delivery.
- State stays local until it needs sharing. Each component owns its own state; when two components need the same data, you lift it to their common parent (coming up shortly).
Solid arrows are props flowing down. Dashed arrows are the one sanctioned way to send information back up: callback functions, which we'll cover next.
Talking Back with Callbacks
If data only flows down, how does a child ever affect its parent? The parent passes a function down as a prop. The child calls that function β often with a value β and the parent's own code runs, updating the parent's state. Data still only flows down; the child just requests a change.
import { useState } from 'react';
function GreetingApp() {
const [name, setName] = useState('');
// This callback is handed to the child
return (
<div>
<h1>Hello, {name || 'stranger'}!</h1>
<NameInput onNameChange={setName} />
</div>
);
}
function NameInput({ onNameChange }) {
return (
<input
type="text"
placeholder="Enter your name"
onChange={(e) => onNameChange(e.target.value)}
/>
);
}
β The convention
Props that pass data down are usually nouns (name, items). Props that send events up are usually functions named with an on prefix (onNameChange, onDelete, onSubmit). Following this makes a component's API self-documenting.
Lifting State Up
When two sibling components need to share the same piece of state, you can't store it in either one β siblings can't see each other's state. The fix is lifting state up: move the state to their nearest common parent, then pass it down to both as props, along with callbacks to update it.
This keeps a single source of truth: one place owns the data, and both children always display the same, consistent value.
π‘ How high should you lift?
Lift state to the lowest component that is a common ancestor of everyone who needs it β no higher. Lifting state too far up makes the app harder to follow and triggers extra re-renders. "As high as necessary, but no higher."
Escaping Prop Drilling with Context
Sometimes a value (the logged-in user, the current theme) is needed by a component buried deep in the tree. Passing it down through every intermediate component that doesn't itself use it is called prop drilling β tedious and noisy.
React's Context API lets a provider high in the tree make a value available to any descendant directly, skipping the middle layers:
import { createContext, useContext, useState } from 'react';
// 1. Create the context
const UserContext = createContext(null);
// 2. A provider component holds the value
function UserProvider({ children }) {
const [user, setUser] = useState({ name: 'Ada Lovelace' });
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
// 3. A custom hook makes reading it clean
function useUser() {
const ctx = useContext(UserContext);
if (!ctx) throw new Error('useUser must be used inside a UserProvider');
return ctx;
}
// 4. Any descendant reads it β no prop drilling
function CommentBox() {
const { user } = useUser();
return <p>Commenting as {user.name}</p>;
}
function App() {
return (
<UserProvider>
<Page /> {/* CommentBox lives somewhere deep inside Page */}
</UserProvider>
);
}
β οΈ Don't over-use Context
Context is for data that's genuinely global-ish β theme, current user, language. For data needed by only one or two nearby components, plain props are simpler and keep components reusable. Reach for Context to solve real prop-drilling pain, not as a default.
| Use props when⦠| Use Context when⦠|
|---|---|
| Data is needed by immediate children | Data is needed by many components at many depths |
| The component should stay reusable | Passing props would mean drilling through many layers |
| Relationships are shallow and clear | The value is app-wide (theme, auth, locale) |
Hands-on: Temperature Converter
ποΈ Share state between two inputs
Objective: Build a converter with a Celsius input and a Fahrenheit input. Typing in either one updates the other. The trick is that both inputs must read from one lifted piece of state.
Requirements:
- A parent
TemperatureConverterowns the temperature value and which scale was last edited. - A reusable
TemperatureInputchild receives the value and anonChangecallback as props. - Editing one input recomputes and displays the other.
π‘ Hint
Store just two things in the parent: the raw temperature string and the scale that was typed into ('c' or 'f'). Derive the other scale's value with a conversion function during render β don't store both temperatures, or they can drift out of sync. This is "single source of truth" in action.
β Solution
import { useState } from 'react';
const toCelsius = (f) => ((f - 32) * 5) / 9;
const toFahrenheit = (c) => (c * 9) / 5 + 32;
function round(value) {
return Number.isNaN(value) ? '' : Math.round(value * 100) / 100;
}
function TemperatureInput({ scale, value, onChange }) {
const label = scale === 'c' ? 'Celsius' : 'Fahrenheit';
return (
<fieldset>
<legend>Temperature in {label}</legend>
<input
type="number"
value={value}
onChange={(e) => onChange(scale, e.target.value)}
/>
</fieldset>
);
}
function TemperatureConverter() {
// Lifted state: one value + which scale it was typed in
const [temperature, setTemperature] = useState('');
const [scale, setScale] = useState('c');
const handleChange = (editedScale, value) => {
setScale(editedScale);
setTemperature(value);
};
const parsed = parseFloat(temperature);
const celsius = scale === 'f' ? round(toCelsius(parsed)) : temperature;
const fahrenheit = scale === 'c' ? round(toFahrenheit(parsed)) : temperature;
return (
<div>
<h2>Temperature Converter</h2>
<TemperatureInput scale="c" value={celsius} onChange={handleChange} />
<TemperatureInput scale="f" value={fahrenheit} onChange={handleChange} />
{!Number.isNaN(parsed) && (
<p>{Number(celsius) >= 100 ? 'The water would boil.' : 'The water would not boil.'}</p>
)}
</div>
);
}
Both inputs are the same reusable child. The parent holds the single source of truth and derives the other value on every render, so the two fields can never disagree.
Summary & Quiz
π Key Takeaways
- Props pass data from parent to child and can be any JavaScript type.
- React uses one-way data flow: data goes down the tree, and props are read-only.
- Children affect parents by calling callback functions passed down as props (named
onSomething). - Lifting state up to a common parent gives siblings a single source of truth.
- The Context API avoids prop drilling for genuinely app-wide data.
π― Quick Quiz
Question 1: In React, which way does data flow through props?
Question 2: How does a child component send data back up to its parent?
Question 3: Two sibling components need to share the same data. What's the standard React solution?
π Further Reading
- react.dev β Passing Props to a Component
- react.dev β Sharing State Between Components
- react.dev β Passing Data Deeply with Context
π What's Next?
You can now move data through a component tree. Next, in Component Composition Patterns, we'll use props (especially children) to combine small components into flexible, reusable systems β the patterns real UI libraries are built on.
π Great progress!
Props and one-way flow are the mental model the rest of React builds on.