Topic #14Foundational14 min read

Props

Props in full: a component's read-only inputs, one-way data flow, callbacks for events-up, default values and destructuring, the children prop, spreading and forwarding, typing props with TypeScript, the props-vs-state distinction, and prop-drilling with its remedies.

#react#props#data-flow#components#children#context#typescript#callbacks

What props are. Props (short for properties) are the inputs to a component. A parent passes data down to a child as props; the child reads them to decide what to render. The clearest way to think about it: a component is a function and props are its single parameter object. <ExpenseCard title="Lunch" amount={1200} /> is essentially calling ExpenseCard({ title: 'Lunch', amount: 1200 }).

Any value can be a prop. Props aren't limited to strings and numbers. You can pass objects, arrays, functions, other React elements, even components themselves. String props use quotes (title="Lunch"); everything else uses braces (amount={1200}, tags={['a','b']}, onApprove={handleApprove}). This flexibility is what makes components configurable and reusable.

Props are read-only. A child must never modify the props it receives — they belong to the parent. React relies on this: components must behave like pure functions with respect to their props. Mutating a prop (e.g. pushing into a prop array) breaks change detection and produces stale, unpredictable renders. If a value needs to change over time and be owned locally, that's state, not props.

One-way data flow. Data flows in a single direction: parent → child. To change what a child displays, you don't reach into the child — you change the parent's state and re-render, which passes new props down. This top-down flow is the backbone of React's predictability: any value on screen can be traced by walking up the tree to whoever owns it.

Events flow up via callbacks. If data only flows down, how does a child affect its parent? The parent passes a function as a prop (a callback like onApprove), and the child invokes it when something happens, optionally passing data back. The parent's handler then updates state, and new props flow back down. The slogan: data flows down, events flow up.

Destructuring and defaults. Idiomatic React destructures props in the parameter list: function Card({ title, amount, category = 'other' }) { ... }. Default values in the destructuring give a prop a fallback when the parent omits it — the modern replacement for the old Component.defaultProps (which is deprecated for function components).

The children prop. children is a special prop containing whatever JSX a parent nests between the component's tags. It enables generic wrappers (<Card>...</Card>). Because it's an ordinary prop, you render it with {children} wherever you want the nested content to appear.

Spreading and forwarding props. The spread operator forwards a whole props object: <Button {...rest} />. This is common for wrapper components that add a little and pass the rest through to a DOM element or inner component. Be intentional — blindly spreading unknown props onto DOM nodes can leak invalid attributes, so many components destructure the props they care about and ...rest the remainder onto the element.

Typing props with TypeScript. In TS you declare a props type/interface and annotate the parameter: function Card({ title }: { title: string }) {} or a named interface CardProps. Optional props use ?, callbacks are typed as functions (onApprove?: (id: number) => void), and children is typed React.ReactNode. Typed props are self-documenting and catch mismatched usage at compile time.

Props vs state (the classic question). Props are inputs passed in from the parent and are read-only from the child's view; state is data a component owns internally and mutates via a setter, which triggers a re-render. Rule of thumb: if the parent controls it, it's a prop; if the component controls it over time, it's state. A common pattern is a parent holding state and passing slices of it down as props (lifting state up).

Prop drilling and its remedies. Passing a prop through many intermediate components that don't use it — only to reach a deep descendant — is prop drilling. It's noisy and fragile. Remedies: restructure with composition (pass elements via children so intermediate layers don't touch the data), use the Context API for widely-shared values (theme, current user), or a state library for complex global state.

The mental model (memorise this). Props are a component's read-only parameters, passed one-way from parent to child. To change a child you change the parent's state; to notify a parent a child calls a callback prop. Data flows down, events flow up. Destructure with defaults, type them, and when a prop is drilled too far, reach for composition or Context.

Backend Analogy

Props are the immutable parameter object / DTO passed into a method: the callee reads them but must not mutate the caller's arguments, and they flow one way (caller → callee). A callback prop is a handler/listener you inject — like passing a Consumer or a functional-interface lambda so the callee can call you back (an Observer or the strategy pattern), which is exactly how events flow up. Typing props with TypeScript is your method signature: the compiler rejects a bad call site. Props-vs-state maps to method arguments (external, per-call) versus instance fields (owned, mutable over the object's lifetime). Prop drilling is threading the same dependency through many constructors by hand — and Context is the DI container that injects a shared dependency wherever it's needed instead.

Key Insights
  • Props are a component's inputs, passed by the parent — think function parameters bundled into one object.
  • Any value can be a prop: strings, numbers, objects, arrays, functions, elements, even components.
  • Props are read-only; a child must never mutate them — mutation breaks change detection.
  • Data flows one way, parent → child; to change a child, change the parent's state and re-render.
  • Events flow up via callback props: the child invokes a function the parent passed down.
  • Destructure props with default values instead of the deprecated defaultProps for function components.
  • children is a normal prop holding nested JSX; render it with {children}.
  • Spread ({...rest}) forwards props, but avoid leaking unknown attributes onto DOM nodes.
  • Type props with TypeScript (interface/type, ? for optional, React.ReactNode for children).
  • Props are external and read-only; state is owned and mutable — and deep prop drilling is a signal to use composition or Context.

Worked Code

Passing and receiving props (data down, events up)
TSX
interface CardProps {
  title: string;
  amount: number;
  category?: 'meals' | 'travel' | 'other'; // optional
  onApprove?: (id: number) => void;         // callback prop
  id: number;
}

// Destructure props with a default value for category.
function ExpenseCard({ title, amount, category = 'other', onApprove, id }: CardProps) {
  return (
    <div className="rounded border p-4">
      <h3>{title}</h3>
      <p>{amount.toFixed(2)}</p>
      <span>{category}</span>
      {/* child invokes the callback -> event flows UP to the parent */}
      {onApprove && <button onClick={() => onApprove(id)}>Approve</button>}
    </div>
  );
}

// Parent passes data DOWN and a callback for events UP.
function List() {
  const approve = (id: number) => console.log('approved', id);
  return (
    <ExpenseCard id={1} title="Client Lunch" amount={1200}
      category="meals" onApprove={approve} />
  );
}
children and spreading/forwarding props
TSX
// children: a generic wrapper renders whatever is nested inside it.
function Panel({ children }: { children: React.ReactNode }) {
  return <div className="panel">{children}</div>;
}

// Forwarding: take the props you care about, spread the rest onto <button>.
type ButtonProps = { variant?: 'primary' | 'ghost' } &
  React.ButtonHTMLAttributes<HTMLButtonElement>;

function Button({ variant = 'primary', ...rest }: ButtonProps) {
  // ...rest = onClick, disabled, type, etc. — forwarded intact.
  return <button className={`btn btn-${variant}`} {...rest} />;
}

// Usage: <Button variant="ghost" disabled onClick={fn}>Save</Button>
Props vs state — where a value lives
TSX
import { useState } from 'react';

// PARENT owns the state (data that changes over time).
function Counter() {
  const [count, setCount] = useState(0);
  // It passes a SLICE of state down as a prop, plus a callback up.
  return <Display value={count} onIncrement={() => setCount(c => c + 1)} />;
}

// CHILD receives count as a read-only PROP; it doesn't own it.
function Display({ value, onIncrement }: { value: number; onIncrement: () => void }) {
  // value is a prop -> read-only. To change it, notify the parent.
  return <button onClick={onIncrement}>Count: {value}</button>;
}
Prop drilling vs Context (the remedy)
TSX
import { createContext, useContext } from 'react';

// PROBLEM: theme drilled through Layout and Toolbar just to reach Button.
// <App theme> -> <Layout theme> -> <Toolbar theme> -> <Button theme />

// REMEDY: Context provides the value to any depth without drilling.
const ThemeContext = createContext<'light' | 'dark'>('light');

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar /> {/* no theme prop threaded through anymore */}
    </ThemeContext.Provider>
  );
}
function Toolbar() { return <ThemedButton />; }
function ThemedButton() {
  const theme = useContext(ThemeContext); // read directly from context
  return <button className={theme}>Themed</button>;
}

Interview-Ready Q&A

Props are inputs passed into a component from its parent and are read-only from the child's perspective. State is data a component owns and manages internally; changing it via its setter triggers a re-render. Props flow down from parent to child; state is local. A common pattern is a parent holding state and passing slices of it down as props (lifting state up).

Things to Remember
  • 1Props are a component's read-only inputs — like a function's parameter object.
  • 2Any value can be a prop: strings, numbers, objects, arrays, functions, elements.
  • 3Never mutate props; if a value changes over time and is owned locally, it's state.
  • 4Data flows down (props), events flow up (callback props).
  • 5To change a child, change the parent's state and re-render.
  • 6Destructure props with default values (defaultProps is deprecated for function components).
  • 7children is a normal prop; render it with {children}.
  • 8Spread props deliberately; don't leak unknown attributes onto DOM nodes.
  • 9Type props with TypeScript: ? for optional, function types for callbacks, React.ReactNode for children.
  • 10Deep prop drilling means reach for composition or the Context API.

References & Further Reading