RizTech Academy logo
RizTech Academy
React FundamentalsLesson 2 of 725 min

Props, and thinking in components

A component rendering one hard-coded product is a picture. Props make it a component: the same markup, different data.

Passing data in

type ProductCardProps = {
  name: string;
  pricePaise: number;
  inStock: boolean;
};

export function ProductCard({ name, pricePaise, inStock }: ProductCardProps) {
  return (
    <article className="rounded-xl border p-4">
      <h2 className="font-semibold">{name}</h2>
      <p>₹{(pricePaise / 100).toFixed(2)}</p>
      <button disabled={!inStock}>
        {inStock ? "Add to cart" : "Out of stock"}
      </button>
    </article>
  );
}
<ProductCard name="Aashirvaad Atta 5kg" pricePaise={28500} inStock />
<ProductCard name="Tata Salt 1kg" pricePaise={2800} inStock={false} />

Strings can use quotes; everything else needs braces. inStock on its own means inStock={true}.

Props are a component's parameters. React collects the attributes into one object and passes it; destructuring in the signature is convention because it reads better than props.name everywhere.

Typing them

Two equivalent styles:

type Props = { name: string; pricePaise: number };
export function ProductCard({ name, pricePaise }: Props) { ... }
export function ProductCard({ name }: { name: string }) { ... }

The inline form is fine for one or two props. Past that, name the type.

Better still, use the shared type from module 1:

import type { Product } from "@kirana/shared";

export function ProductCard({ product }: { product: Product }) {
  return <h2>{product.name}</h2>;
}

One product prop beats six separate ones. Adding a field means changing the shared type, not every call site — and the compiler finds anything that breaks.

Optional props with defaults:

type Props = {
  product: Product;
  showImage?: boolean;
};

export function ProductCard({ product, showImage = true }: Props) { ... }

Props are read-only

export function ProductCard({ product }: { product: Product }) {
  product.name = product.name.toUpperCase();   // do not
  return <h2>{product.name}</h2>;
}

That mutates the caller's object. React does not stop you, and it breaks things: the parent does not know it changed, other components sharing the object see it change, and re-renders produce inconsistent output.

Treat props as immutable. Derive instead:

const displayName = product.name.toUpperCase();

If a child needs to change something, it asks the parent to — which is the next section.

Passing functions down

Data flows down. Events flow up, as functions:

type Props = {
  product: Product;
  onAddToCart: (productId: string) => void;
};

export function ProductCard({ product, onAddToCart }: Props) {
  return (
    <button onClick={() => onAddToCart(product.id)}>
      Add to cart
    </button>
  );
}
<ProductCard product={product} onAddToCart={(id) => console.log("add", id)} />

The card does not know what adding to a cart means, and does not need to. It reports that a button was pressed; the parent decides.

This is the most important idea in React composition. A component that knows only about its own props and calls back for everything else can be used anywhere — in a product grid, in search results, in an admin screen — without modification.

The naming convention is onSomething for the prop and handleSomething for the function passed in.

children

Content between the tags arrives as children:

export function Card({ children }: { children: React.ReactNode }) {
  return <div className="rounded-xl border p-4">{children}</div>;
}
<Card>
  <h2>Atta</h2>
  <p>₹285.00</p>
</Card>

React.ReactNode covers anything renderable — elements, strings, numbers, arrays, null.

This is how you write layout components that do not care what goes inside them, and it is the main tool for avoiding prop explosion. More in the composition lesson.

Spreading props

const product = { id: "1", name: "Atta", pricePaise: 28500 };
<ProductCard {...product} />

Same ... as in JavaScript generally. It is concise and it hides what is being passed, so a reader must go and look. Use it for genuine pass-through — forwarding button attributes — and write props explicitly otherwise.

Prop drilling

<Page user={user}>
  <Layout user={user}>
    <Header user={user}>
      <UserMenu user={user} />

Three components pass user along without using it. That is prop drilling, and past two or three levels it becomes tedious and noisy.

The answers, in order of preference: restructure using children so the data does not have to travel; React Context for genuinely global values like the current user or theme; a state library for complex shared state. Module 4 covers the choice.

Do not reach for Context immediately. Two levels of drilling is fine and explicit; Context makes data flow invisible, which has its own cost.

Check your work

Why one product prop beats six: adding a field means changing the shared type rather than every call site, and the compiler finds what breaks.

Why mutating a prop is wrong even when nothing visibly breaks: the parent does not know it changed, anything else sharing the object sees the change, and re-renders can produce inconsistent output.

How a child reports an event: it calls a function passed in as a prop — onAddToCart — so it never needs to know what adding to a cart means.

What children is for: content between the tags, typed as React.ReactNode. It lets a component wrap anything without knowing what.

How to remove prop drilling: restructure with children so the data does not travel through components that do not use it. Context is the fallback, not the first move.

Removing a required prop: TypeScript reports the missing property at the call site, which is the whole reason for typing props.

Practice

  1. Convert your hard-coded ProductCard to take name, pricePaise and inStock. Render three different products.
  2. Replace those with one product prop typed from @kirana/shared.
  3. Add an optional showImage prop defaulting to true.
  4. Mutate a prop inside the component. Nothing visibly breaks at first — explain why it is still wrong.
  5. Add onAddToCart and log the id from the parent.
  6. Build a generic Card that renders children, and put a ProductCard inside it.
  7. Pass a prop through three levels without using it in the middle two. Then restructure with children so it does not have to travel.
  8. Remove a required prop from one call site and read the TypeScript error.

Next: state — data that changes while the page is open.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship