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
- Convert your hard-coded
ProductCardto takename,pricePaiseandinStock. Render three different products. - Replace those with one
productprop typed from@kirana/shared. - Add an optional
showImageprop defaulting to true. - Mutate a prop inside the component. Nothing visibly breaks at first — explain why it is still wrong.
- Add
onAddToCartand log the id from the parent. - Build a generic
Cardthat renderschildren, and put aProductCardinside it. - Pass a prop through three levels without using it in the middle two. Then
restructure with
childrenso it does not have to travel. - 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