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:

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
  • active alone means active={true}
  • Default values: active = true in destructuring

Compare Vue defineProps.

Callback Props (Events Up)

TodoItem.tsx:

Parent owns state and passes handlers:

tsx
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

tsx
// 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:

tsx
function Parent() {
  const [selectedId, setSelectedId] = useState<string | null>(null);
 
  return (
    <>
      <Sidebar selectedId={selectedId} onSelect={setSelectedId} />
      <DetailPanel id={selectedId} />
    </>
  );
}
text
        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:

DepthSolution
1–2 levelsKeep drilling
Many layersContext API or Zustand
Theme / authContext or global store

Spreading Props

tsx
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

text
src/components/
├── UserCard.tsx
├── TodoItem.tsx
└── ui/
    └── Button.tsx

One 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?

tsx
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.

Next chapter?

Children and Compound Components.