Passing Functions as Props
Callback props - how a child tells its parent that something happened.
What you will be able to do
- Pass a function as a prop, and call it from the child
- Explain why the parent, not the child, should own the behaviour
- Tell passing a function from calling it - and read the TypeScript error when you get it wrong
- Pass arguments back to the parent, with an inline arrow when needed
- Type callback props, including event objects and optional callbacks
- Name callback props and handlers consistently
- Build a reusable component that knows nothing about what its buttons do
The idea, in plain English
Props move data down the tree. But things happen at the bottom: the user clicks Start Learning inside a CourseCard. The card knows the click happened, yet it should not decide what it means - on the catalogue page it might open the course, on the admin page it might edit it. That decision belongs to the parent.
The fix is to pass a function down. The parent writes handleStart and passes it as a prop - <CourseCard onStart={handleStart} />. The card calls onStart() when the button is clicked, and the parent code runs. Data went down as a prop; the event came back up as a call.
A function passed to be called later is a callback, and a prop holding one is a callback prop. There is nothing special about it to React: onStart is an ordinary prop that happens to hold a function. It is the same mechanism as onClick on a button - you have been passing callbacks since Lesson 9.
The parents in this lesson use useState so you can see the result of a callback on the page. Module 3 covers state properly; here you only need to know that setting it re-renders the component with the new value.
Worked example: Course cards that report Start, Enroll, and Complete to the page that owns them.
Passing a function down
In the parent, define the behaviour and pass it: function handleStart() { ... } then <CourseCard title="React" onStart={handleStart} />. In the child, type it and call it: onStart: () => void in the props type, and <button onClick={onStart}> in the JSX.
Follow the click: the user clicks the button; React calls the button onClick, which is onStart; onStart is the parent handleStart, so the parent code runs. The child never needs to know what handleStart does.
Pass it, do not call it
onStart={handleStart} passes the function. onStart={handleStart()} calls it immediately, during render, and passes whatever it returns - usually undefined. The code runs on every render, and the button then does nothing when clicked.
TypeScript catches it: the prop expects () => void and receives void, so it reports "Type void is not assignable to type () => void". Without types it fails silently, which is why this is such a common interview question.
Watch out: handleStart is the function. handleStart() is what the function returns. A prop that expects a callback almost always wants the first.
Sending data back up
Callbacks can take arguments, which is how the child tells the parent which thing, or what value. Type it as onSelect: (courseId: number) => void, and call it with the value: onSelect(course.id).
Because the call needs an argument, the child wraps it in an arrow: onClick={() => onSelect(course.id)}. The arrow is the function React calls on click; inside it, onSelect is called with the right id. Writing onClick={onSelect(course.id)} would call it during render - the same mistake as before.
When the child already has everything the parent needs, it can pass several values: onRate(courseId, stars). Keep the argument list short; past two or three values, pass one object.
Event objects
When React calls a DOM handler, it passes an event object. <button onClick={onStart}> therefore calls onStart(event) - your callback receives the click event even though its type says () => void. TypeScript allows this, since a function may ignore arguments it is given.
That is usually fine, but it matters when the callback takes its own first argument. If onSelect expects a course id and you write onClick={onSelect}, it receives the click event as the "id". Wrap it: onClick={() => onSelect(course.id)}.
When the child genuinely needs the event - to read an input value, or prevent a form submit - type it: onChange: (event: ChangeEvent<HTMLInputElement>) => void. The event types are covered in full in Module 3.
Naming: on for props, handle for handlers
The convention is on + event for the prop, and handle + event for the function that handles it: <CourseCard onStart={handleStart} onEnroll={handleEnroll} />. It mirrors the built-in onClick and onChange, so anyone reading a component can tell callback props apart from data props at a glance.
Name callbacks after what happened in the child - onStart, onDelete, onSelect - not after what the parent will do with it - not onNavigateToCoursePage. The child reports; the parent decides.
Optional callbacks
Not every parent cares about every event. Mark the callback optional - onDismiss?: () => void - and call it with optional chaining: onDismiss?.(). If the parent did not pass one, nothing happens and nothing throws.
Calling an optional callback without ?. is a TypeScript error ("Cannot invoke an object which is possibly undefined"), which is exactly the reminder you want.
Callbacks that return values
A callback can return something the child uses: canStart: (courseId: number) => boolean lets the card ask the parent whether to enable its button. Most callbacks return nothing - they report an event - and are typed () => void.
Returning values works, but if a child keeps asking its parent questions, consider passing the answer down as a prop instead: canStart={true} is simpler than a function the child has to call.
Reusable components stay ignorant
Callbacks are what make components reusable. A ConfirmButton with an onConfirm prop can delete a lesson on one page and publish a course on another; it only knows that it was confirmed.
A component that hard-codes its behaviour - a CourseCard that calls navigate("/courses/react") itself - can only ever do that one thing. Push the decision up to the parent, and the component works anywhere.
Syntax and examples
type CourseCardProps = {
title: string;
onStart: () => void;
};
function CourseCard({ title, onStart }: CourseCardProps) {
return (
<article>
<h2>{title}</h2>
<button onClick={onStart}>Start Learning</button>
</article>
);
}
function CoursePage() {
function handleStart() {
console.log("Course started");
}
return <CourseCard title="React" onStart={handleStart} />;
}
// click -> onStart -> handleStart -> "Course started"<CourseCard title="React" onStart={handleStart} />;
// Passes the function. It runs when the button is clicked.
<CourseCard title="React" onStart={handleStart()} />;
// Calls it now, during render, and passes the result (undefined).
// TypeScript: Type 'void' is not assignable to type '() => void'.
// Without types: "Course started" logs on every render, and clicking does nothing.import { useState } from "react";
type Lesson = { id: number; title: string };
type LessonRowProps = {
lesson: Lesson;
completed: boolean;
onComplete: (lessonId: number) => void;
};
function LessonRow({ lesson, completed, onComplete }: LessonRowProps) {
return (
<li>
{lesson.title}{" "}
{completed ? (
<span>✓</span>
) : (
<button onClick={() => onComplete(lesson.id)}>Mark complete</button>
)}
</li>
);
}
const lessons: Lesson[] = [
{ id: 1, title: "JSX" },
{ id: 2, title: "Props" },
];
function LessonList() {
const [completedIds, setCompletedIds] = useState<number[]>([]); // state - Module 3
function handleComplete(lessonId: number) {
setCompletedIds([...completedIds, lessonId]);
}
return (
<>
<ul>
{lessons.map((lesson) => (
<LessonRow
key={lesson.id}
lesson={lesson}
completed={completedIds.includes(lesson.id)}
onComplete={handleComplete}
/>
))}
</ul>
<p>
{completedIds.length} of {lessons.length} complete
</p>
</>
);
}type Props = {
courseId: number;
onSelect: (courseId: number) => void;
};
function CourseButton({ courseId, onSelect }: Props) {
// Wrong: React calls onSelect(clickEvent) - the parent receives an event, not an id.
// TypeScript rejects it here, because the types do not match:
// <button onClick={onSelect}>Select</button>
// Right: an arrow that passes the id
return <button onClick={() => onSelect(courseId)}>Select</button>;
}import type { ChangeEvent, MouseEvent } from "react";
type SearchBoxProps = {
onSearch: (query: string) => void;
};
function SearchBox({ onSearch }: SearchBoxProps) {
function handleChange(event: ChangeEvent<HTMLInputElement>) {
onSearch(event.target.value); // send the value up, not the event
}
return <input placeholder="Search courses" onChange={handleChange} />;
}
type ToolbarProps = {
onAction: (event: MouseEvent<HTMLButtonElement>) => void; // when the parent needs the event
};
function Toolbar({ onAction }: ToolbarProps) {
return <button onClick={onAction}>Run</button>;
}type BannerProps = {
message: string;
onDismiss?: () => void;
};
function Banner({ message, onDismiss }: BannerProps) {
return (
<div role="status">
{message}
{onDismiss && <button onClick={() => onDismiss?.()}>Dismiss</button>}
</div>
);
}
<Banner message="New lesson added" />; // no button
<Banner message="New lesson added" onDismiss={handleDismiss} />; // with button
// onDismiss() without ?. - TypeScript: Cannot invoke an object which is possibly 'undefined'.type Course = { id: number; title: string; enrolled: boolean };
type CourseCardProps = {
course: Course;
onStart: (courseId: number) => void;
onEnroll: (courseId: number) => void;
onShare?: (courseId: number) => void;
};
function CourseCard({ course, onStart, onEnroll, onShare }: CourseCardProps) {
return (
<article>
<h2>{course.title}</h2>
{course.enrolled ? (
<button onClick={() => onStart(course.id)}>Continue</button>
) : (
<button onClick={() => onEnroll(course.id)}>Enroll</button>
)}
{onShare && <button onClick={() => onShare(course.id)}>Share</button>}
</article>
);
}type StartButtonProps = {
courseId: number;
canStart: (courseId: number) => boolean;
onStart: (courseId: number) => void;
};
function StartButton({ courseId, canStart, onStart }: StartButtonProps) {
const allowed = canStart(courseId); // the child asks the parent
return (
<button disabled={!allowed} onClick={() => onStart(courseId)}>
{allowed ? "Start" : "Locked"}
</button>
);
}
// Often simpler: let the parent compute it and pass allowed={...} as a plain prop.type ConfirmButtonProps = {
label: string;
question: string;
onConfirm: () => void;
};
function ConfirmButton({ label, question, onConfirm }: ConfirmButtonProps) {
function handleClick() {
if (window.confirm(question)) {
onConfirm();
}
}
return <button onClick={handleClick}>{label}</button>;
}
// The same component, two unrelated jobs:
<ConfirmButton label="Delete lesson" question="Delete this lesson?" onConfirm={() => deleteLesson(7)} />;
<ConfirmButton label="Publish" question="Publish this course?" onConfirm={publishCourse} />;Tip: When a child needs to "change" something it received as a prop, the answer is almost always a callback: the child reports what happened, and the parent - which owns the data - changes it.
Callback prop patterns
No argumentsReport that something happened.
onStart: () => void
With a valueReport which item, or what value.
onSelect: (id: number) => void
Arrow in the childNeeded when you pass arguments.
onClick={() => onSelect(id)}OptionalThe parent may not care.
onDismiss?: () => void onDismiss?.()
Needs the eventType the event parameter.
(e: ChangeEvent<HTMLInputElement>) => void
Returns a valueThe child asks the parent.
canStart: (id: number) => boolean
Namingon... for props, handle... for handlers.
onStart={handleStart}Try it yourself
The code does not change. Swap the content string and the program does something else entirely.
“Change onStart={handleStart} to onStart={handleStart()} with a console.log in handleStart. Read the TypeScript error, then remove the type and watch when it logs.”
“Give CourseCard an onSelect: (id: number) => void prop and show the selected course title in the parent.”
“In an untyped version, write onClick={onSelect} and log what onSelect receives. Then switch to onClick={() => onSelect(course.id)}.”
“Make onShare optional in CourseCard, render one card with it and one without, and check that neither throws.”
What usually goes wrong
The function runs during render, and the prop receives its return value.
✗ <CourseCard onStart={handleStart()} />✓ <CourseCard onStart={handleStart} />onSelect(course.id) runs immediately. Wrap it so it runs on click.
✗ <button onClick={onSelect(course.id)}>✓ <button onClick={() => onSelect(course.id)}>React calls it with the click event, so the parent receives an event instead of an id.
✗ <button onClick={onSelect}>✓ <button onClick={() => onSelect(course.id)}>If the parent did not pass it, the call throws. Use optional chaining.
✗ onDismiss();✓ onDismiss?.();Hard-coding navigation or deletion in the child ties it to one page. Report the event and let the parent decide.
✗ <button onClick={() => navigate("/courses/react")}>Start</button>✓ <button onClick={() => onStart(course.id)}>Start</button>Props are read-only. The child calls a callback; the parent updates its own data and passes the new value down.
✗ props.completed = true;✓ onComplete(lesson.id);Best practices
- Keep behaviour in the component that owns the data, and pass it down as a callback.
- Name callback props on... and handlers handle....
- Type every callback prop, including its arguments.
- Send values up, not events, unless the parent really needs the event.
- Mark callbacks optional when a parent may not care, and call them with ?.().
- Name callbacks after what happened in the child, not what the parent will do.
Practice
Write these yourself before opening anything. Getting them wrong first is most of how this sticks.
Build a CourseCard with title and onStart props, and a CoursePage that logs "Starting React" when the button is clicked.
Show hintHide hint
The card calls onStart; the page decides what happens.
Show solutionHide solution
type CourseCardProps = {
title: string;
onStart: () => void;
};
function CourseCard({ title, onStart }: CourseCardProps) {
return (
<article>
<h2>{title}</h2>
<button onClick={onStart}>Start Learning</button>
</article>
);
}
function CoursePage() {
function handleStart() {
console.log("Starting React");
}
return <CourseCard title="React" onStart={handleStart} />;
}Render three courses from an array, each with a Select button. Show "Selected: <title>" in the parent, or "Nothing selected".
Show hintHide hint
onSelect: (id: number) => void, an arrow in the child, and a piece of state in the parent.
Show solutionHide solution
import { useState } from "react";
type Course = { id: number; title: string };
const courses: Course[] = [
{ id: 1, title: "React" },
{ id: 2, title: "TypeScript" },
{ id: 3, title: "Node.js" },
];
function CourseItem({ course, onSelect }: { course: Course; onSelect: (id: number) => void }) {
return (
<li>
{course.title} <button onClick={() => onSelect(course.id)}>Select</button>
</li>
);
}
function CoursePicker() {
const [selectedId, setSelectedId] = useState<number | null>(null);
const selected = courses.find((course) => course.id === selectedId);
return (
<>
<ul>
{courses.map((course) => (
<CourseItem key={course.id} course={course} onSelect={setSelectedId} />
))}
</ul>
<p>{selected ? "Selected: " + selected.title : "Nothing selected"}</p>
</>
);
}Find the three bugs: <LessonRow onComplete={markComplete(lesson.id)} onDelete={deleteLesson} /> where LessonRow renders <button onClick={onDelete}>Delete</button> and deleteLesson expects a lesson id.
Show hintHide hint
One call happens too early, and one function receives the wrong argument.
Show solutionHide solution
// 1. markComplete(lesson.id) runs during render; the prop gets its return value.
// 2. onClick={onDelete} makes React call deleteLesson(clickEvent), not deleteLesson(id).
// 3. So the Delete button deletes nothing - it passes an event where an id was expected.
<LessonRow lesson={lesson} onComplete={markComplete} onDelete={deleteLesson} />;
function LessonRow({ lesson, onComplete, onDelete }: LessonRowProps) {
return (
<li>
{lesson.title}
<button onClick={() => onComplete(lesson.id)}>Complete</button>
<button onClick={() => onDelete(lesson.id)}>Delete</button>
</li>
);
}A course list that reports everything up
Build a list of course cards where every action is decided by the page, and the cards only report what was clicked.
- Each course has id, title, and enrolled.
- CourseCard takes course, onEnroll, onStart, and an optional onShare.
- An enrolled course shows Continue; otherwise Enroll.
- Share appears only when onShare is passed.
- The page keeps a log of actions, such as "Enrolled in Node.js", and shows it under the list.
- Enrolling moves a course to enrolled, so its button changes to Continue.
import { useState } from "react";
type Course = { id: number; title: string; enrolled: boolean };
const initialCourses: Course[] = [
{ id: 1, title: "React", enrolled: true },
{ id: 2, title: "Node.js", enrolled: false },
];
function CourseCard(/* course, onEnroll, onStart, onShare? */) {}
function CoursePage() {
const [courses, setCourses] = useState(initialCourses);
const [log, setLog] = useState<string[]>([]);
// handlers, then render
}Show one solutionHide solution
import { useState } from "react";
type Course = { id: number; title: string; enrolled: boolean };
type CourseCardProps = {
course: Course;
onEnroll: (courseId: number) => void;
onStart: (courseId: number) => void;
onShare?: (courseId: number) => void;
};
function CourseCard({ course, onEnroll, onStart, onShare }: CourseCardProps) {
return (
<article>
<h2>{course.title}</h2>
{course.enrolled ? (
<button onClick={() => onStart(course.id)}>Continue</button>
) : (
<button onClick={() => onEnroll(course.id)}>Enroll</button>
)}
{onShare && <button onClick={() => onShare(course.id)}>Share</button>}
</article>
);
}
const initialCourses: Course[] = [
{ id: 1, title: "React", enrolled: true },
{ id: 2, title: "Node.js", enrolled: false },
];
function CoursePage() {
const [courses, setCourses] = useState(initialCourses);
const [log, setLog] = useState<string[]>([]);
function titleOf(courseId: number) {
return courses.find((course) => course.id === courseId)?.title ?? "";
}
function handleEnroll(courseId: number) {
setCourses(courses.map((course) => (course.id === courseId ? { ...course, enrolled: true } : course)));
setLog([...log, "Enrolled in " + titleOf(courseId)]);
}
function handleStart(courseId: number) {
setLog([...log, "Continued " + titleOf(courseId)]);
}
function handleShare(courseId: number) {
setLog([...log, "Shared " + titleOf(courseId)]);
}
return (
<main>
{courses.map((course) => (
<CourseCard
key={course.id}
course={course}
onEnroll={handleEnroll}
onStart={handleStart}
onShare={handleShare}
/>
))}
<ol>
{log.map((entry, index) => (
<li key={index}>{entry}</li>
))}
</ol>
</main>
);
}Key points
- A function can be passed as a prop; the child calls it, and the parent code runs.
- Data flows down as props; events flow up as callback calls.
- Pass the function (handleStart), do not call it (handleStart()).
- Use an arrow in the child to pass arguments: onClick={() => onSelect(id)}.
- DOM handlers receive an event object - wrap callbacks that expect something else.
- Type callbacks: () => void, (id: number) => void; optional ones with ? and ?.().
- Name props on... and handlers handle....
- Components that report events instead of deciding them can be reused anywhere.
Quick check before you move on
Interview questions
How does a child component communicate with its parent?
The parent passes a function as a prop, and the child calls it - often from an event handler, with any values the parent needs. Data flows down as props, and events flow up as calls.
What is the difference between onClick={handleClick} and onClick={handleClick()}?
The first passes the function for React to call on click. The second calls it during render and passes its return value, so it runs on every render and the click does nothing.
How do you pass arguments to a callback from a click?
Wrap the call in an arrow: onClick={() => onSelect(course.id)}. React calls the arrow on click, and the arrow calls the callback with the argument.
How do you type callback props in TypeScript?
As function types in the props type: onStart: () => void, onSelect: (id: number) => void, and optional ones with ?. Event parameters use React types such as ChangeEvent<HTMLInputElement>.
Why should the parent own the behaviour rather than the child?
The parent owns the data and knows the context. A child that only reports events can be reused on any page, while one that hard-codes navigation or deletion is tied to a single use.
What naming convention do callback props follow?
on plus the event for the prop - onStart, onDelete - and handle plus the event for the parent function - handleStart. It mirrors onClick and onChange and makes callbacks easy to spot.
Quiz
- 1.
What is a callback prop? A. A prop holding a function that the child calls later B. A prop React calls automatically C. A prop that returns JSX D. A special React API
- 2.
Which passes the function correctly? A. onStart={handleStart()} B. onStart={handleStart} C. onStart="handleStart" D. onStart={() => handleStart}
- 3.
The child must send a lesson id to the parent. Which is right? A. onClick={onComplete(lesson.id)} B. onClick={onComplete} C. onClick={() => onComplete(lesson.id)}
- 4.
How do you type a callback that receives a course id and returns nothing? A. onSelect: number B. onSelect: (courseId: number) => void C. onSelect: () => number D. onSelect: void
- 5.
onShare is optional. Which call is safe? A. onShare() B. onShare?.() C. onShare!() D. onShare.call()
Comments
Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.
Loading comments...
AI
System Design
Backend
- GraphQL8 modules · 69 lessons planned
- Core Python13 modules · 75 lessons planned
- FastAPI5 sections · 20 lessons
- Node.js14 modules · 206 lessons planned
- Node.js Performance7 chapters · 36 topics
- Event Loop Lifecycle6 phases · 3 scenarios
- Docker & Containerization11 modules · 144 lessons planned
- AWS for Developers14 modules · 219 lessons planned
- CI/CD & DevOps Automation10 modules · 134 lessons planned