Composition, Props, and Lifting State
Introduction
Props pass read-only data from parent to child. Callbacks pass events up—React’s alternative to Vue emit. When siblings need shared state, lift state up to their closest common parent. This chapter covers typing props, children, and one-way data flow.
Prerequisites
Basic Props
UserCard.tsx:
App.tsx:
import { UserCard } from "./components/UserCard";
export default function App() {
return <UserCard name="Ada Lo" email="ada@example.com" active />;
}Code explanation:
- Destructure props in the parameter list
activealone meansactive={true}- Default values:
active = truein destructuring
Compare Vue defineProps.
Callback Props (Events Up)
TodoItem.tsx:
Parent owns state and passes handlers:
function toggleTodo(id: string) {
setTodos((prev) =>
prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
);
}Naming convention: onXxx for callback props (onSave, onDelete).
Do Not Mutate Props
// Wrong — mutates parent's object if shared reference
function Bad({ user }: { user: User }) {
user.name = "Hack";
return <p>{user.name}</p>;
}Child components treat props as read-only. Parent updates state; new props flow down.
Lifting State Up
Two siblings need the same selectedId:
function Parent() {
const [selectedId, setSelectedId] = useState<string | null>(null);
return (
<>
<Sidebar selectedId={selectedId} onSelect={setSelectedId} />
<DetailPanel id={selectedId} />
</>
);
} Parent (owns selectedId)
/ \
Sidebar DetailPanel
onSelect(id) id={selectedId}Move state to the lowest ancestor that needs it—no higher than necessary.
Props Drilling
Passing props through many layers gets noisy. Alternatives:
| Depth | Solution |
|---|---|
| 1–2 levels | Keep drilling |
| Many layers | Context API or Zustand |
| Theme / auth | Context or global store |
Spreading Props
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: "primary" | "secondary";
};
export function Button({ variant = "primary", className, ...rest }: ButtonProps) {
return (
<button className={`btn btn--${variant} ${className ?? ""}`} {...rest} />
);
}...rest forwards disabled, aria-*, etc.
Component File Organization
src/components/
├── UserCard.tsx
├── TodoItem.tsx
└── ui/
└── Button.tsxOne component per file is common; co-locate small private helpers in the same file.
FAQ
props vs state?
Props from parent; state owned inside the component.
Optional props?
type Props = { title: string; subtitle?: string };Children as prop?
See Children and Compound Components.
React.FC?
Modern style: annotate props type on the function—function App(props: AppProps)—instead of React.FC.
Key on list of components?
Put key on the element returned from map, not inside the child file.