Module 2: JSX and Components
Props, Children and Composition
How data flows into a component, why props are read-only, and how composition replaces inheritance in React.
Props are the arguments to a component. If a function component is a function, props are its single parameter: an object holding everything the parent passed in.
function Badge({ label, color = "gray" }) {
return <span style={{ background: color }}>{label}</span>;
}
<Badge label="New" color="green" />
<Badge label="Draft" />
React collects all the attributes into one object, { label: "New", color: "green" }, and calls Badge with it. Destructuring in the parameter list is just the usual way to unpack that object.
Props can be anything#
Strings go in quotes. Everything else goes in braces.
<Product
name="Backpack" // string
price={1299} // number
inStock={true} // boolean (same as just writing `inStock`)
tags={["travel", "bag"]} // array
seller={{ id: 4, name: "Acme" }} // object
onBuy={() => addToCart(7)} // function
/>
Passing functions as props is how a child talks back to its parent. The child doesn't know or care what the function does. It just calls it.
Props are read-only#
A component must never modify its own props. If you try, you're mutating something that belongs to the parent, and React has no way to know it changed.
function Bad({ user }) {
user.name = user.name.toUpperCase(); // changes the parent's object
return <p>{user.name}</p>;
}
If you need a different version of the value, compute a new one:
function Good({ user }) {
const shout = user.name.toUpperCase();
return <p>{shout}</p>;
}
When the parent wants different props, it passes different props, and React calls the child again. The data doesn't change inside the child. The child just gets called with something else.
The children prop#
Whatever you put between a component's opening and closing tags arrives as props.children.
function Card({ title, children }) {
return (
<section className="card">
<h3>{title}</h3>
<div className="card-body">{children}</div>
</section>
);
}
<Card title="Shipping">
<p>Arrives in 3 days.</p>
<button>Track</button>
</Card>
Card has no idea what's inside, and doesn't need to. It provides structure (border, padding, heading) and lets the caller supply the content. This is the most important pattern in React component design, and it's worth building the habit.
Composition instead of inheritance#
If you come from object-oriented languages, your instinct might be to make a BaseButton, then extend it into PrimaryButton and DangerButton. React teams almost never do that. Instead, you build small components that wrap or combine each other.
function Button({ variant = "primary", children, ...rest }) {
return <button className={`btn btn-${variant}`} {...rest}>{children}</button>;
}
function DeleteButton(props) {
return <Button variant="danger" {...props}>Delete</Button>;
}
Two things to notice. First, ...rest collects any props the component doesn't care about (like onClick, disabled, type) and forwards them to the real button. Second, the specialized component is just a function that calls the general one with some props preset. No class hierarchy, no overriding.
Passing JSX as a prop#
children is just one slot. You can create as many as you like by passing elements through ordinary props.
function Layout({ header, sidebar, children }) {
return (
<div className="layout">
<header>{header}</header>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);
}
<Layout
header={<Navbar user={user} />}
sidebar={<Filters />}
>
<ProductGrid />
</Layout>
This solves a lot of what people use global state for. If Navbar needs user, and Layout sits in between but doesn't use it, you avoid passing user through Layout by building <Navbar user={user} /> at the top and handing over the finished element.
Without slots: App ──user──► Layout ──user──► Header ──user──► Avatar
With slots: App builds <Avatar user={user} /> and passes it down as an element
Prop drilling#
Passing a prop through several layers that don't use it is called prop drilling. Two or three levels is perfectly fine. It's explicit, and you can see where data comes from. When it gets painful, try these in order:
- Restructure with
childrenand slots as above. - Move the state closer to where it's used.
- Use context (module 4) or a state library (module 6).
Reaching for global state first is a common overcorrection.
Spreading props#
<Button {...props} /> forwards everything. It's handy for wrapper components, and slightly risky, because you also forward things you didn't intend (like an id meant for the wrapper). Use it for thin wrappers, and be explicit everywhere else.
Props change means a new call#
When a parent re-renders, it creates new props objects for its children. By default, the child renders again even if the values are identical. That's normal and mostly cheap. It becomes relevant when you pass new object or function literals on every render to something that's trying to skip unnecessary renders. We'll tackle that in the memoization lesson.
Where people get burned#
- Copying a prop into state with
useState(props.value)and then being confused when the state doesn't follow later changes. The initial value is only used on the first render. - Mutating a prop object or array.
- Forgetting that
undefinedtriggers defaults andnulldoes not. - Passing a function call instead of a function, like
onClick={handle()}, which runs immediately during render. - Building a component with fifteen props. That's usually a sign it should be split, or should accept
children.
Practice#
- Build a
Cardcomponent with atitleprop andchildren. Use it three times with different content, including anotherCardnested inside. - Create
Buttonwithvariantand...rest, and makePrimaryButtonandDangerButtonfrom it without duplicating markup. - Write a
Layoutwithheader,sidebarandchildrenslots. Pass a component instance into each. - Intentionally mutate a prop object in a child and see what happens in the parent. Then fix it.