📓 3.3.2.4 Passing Data Via Callbacks
In the last lesson, we moved mainTicketList into TicketControl and passed it down to TicketList as a prop. Now we'll replace that hardcoded list with real state, and wire up the form so users can actually add tickets. Here's what we'll do:
- Convert
mainTicketListinTicketControlfrom a hardcoded constant to state. - Create a function in
TicketControlthat takes form data and adds a new ticket to the list. - Pass this function down to
NewTicketFormas a prop. - Call this function in
NewTicketFormwhen the form is submitted.
Despite the relatively small amount of code being added, we are working with challenging new concepts. Be patient with yourself and follow along slowly. If it doesn't all click immediately (and it probably won't), trust the process and keep practicing these concepts.
Step 1: Convert mainTicketList to State
In the previous lesson, we moved mainTicketList into TicketControl. Now we'll replace it with state so the list can grow as users add tickets. Remove the mainTicketList constant and add a useState call inside the component:
import { useState } from 'react';
import NewTicketForm from './NewTicketForm';
import TicketList from './TicketList';
import { type TicketData } from '../types';
function TicketControl() {
const [formVisibleOnPage, setFormVisibleOnPage] = useState(false);
const [mainTicketList, setMainTicketList] = useState<TicketData[]>([]);
const handleClick = () => {
setFormVisibleOnPage(!formVisibleOnPage);
}
let currentlyVisibleState;
let buttonText;
if (formVisibleOnPage) {
currentlyVisibleState = <NewTicketForm />;
buttonText = "Return to Ticket List";
} else {
currentlyVisibleState = <TicketList ticketList={mainTicketList} />;
buttonText = "Add Ticket";
}
return (
<>
{currentlyVisibleState}
<button onClick={handleClick}>{buttonText}</button>
</>
);
}
export default TicketControl;
The queue now starts empty - no hardcoded tickets. We use useState<TicketData[]>([]) rather than just useState([]) because TypeScript can't infer the type from an empty array. The explicit generic <TicketData[]> tells TypeScript this state (mainTicketList) holds an array of TicketData objects, allowing TypeScript to catch anything that doesn't match.
Step 2: Create a Function to Handle Adding Tickets
Now let's create a function in TicketControl that will handle adding new tickets to our list:
import { useState } from 'react';
import NewTicketForm from './NewTicketForm';
import TicketList from './TicketList';
import { type TicketData } from '../types';
function TicketControl() {
const [formVisibleOnPage, setFormVisibleOnPage] = useState(false);
const [mainTicketList, setMainTicketList] = useState<TicketData[]>([]);
const handleClick = () => {
setFormVisibleOnPage(!formVisibleOnPage);
}
const handleAddingNewTicketToList = (newTicket: TicketData) => {
const newMainTicketList = mainTicketList.concat(newTicket);
setMainTicketList(newMainTicketList);
setFormVisibleOnPage(false);
}
// ... rest of component
}
Our new function is called handleAddingNewTicketToList because it does just that - handles the process of adding a new ticket to our mainTicketList. It takes a newTicket as a parameter, which we've typed as a TicketData. Because we imported the TicketData type, TypeScript will make sure that anything passed to this function is a complete, valid ticket.
It's common practice to prefix the name of an event handler function with handle. Any props containing that function will be prefixed with on. This is because the prop will be used when the event occurs, but the function itself is what actually handles the necessary actions. It also ensures the names are similar enough to easily determine which props and functions correspond, yet different enough to tell them apart.
Let's break down what this function does:
-
Create a new array: We call
mainTicketList.concat(newTicket). Unlikepush(), which directly alters the array it's called on,concat()returns a new array with the item added. This is important because we should never alter state directly. You'll also commonly see the spread operator used for this:[...mainTicketList, newTicket]. Both approaches create a new array without mutating state. -
Update state: We call
setMainTicketList(newMainTicketList)to update our state with the new array. -
Hide the form: We call
setFormVisibleOnPage(false)so the user sees the queue (with their new ticket) instead of the form.
Step 3: Pass the Function Down as a Prop
Now we need to pass handleAddingNewTicketToList down to our NewTicketForm component as a prop:
// ...existing code
let currentlyVisibleState;
let buttonText;
if (formVisibleOnPage) {
currentlyVisibleState = <NewTicketForm onNewTicketCreation={handleAddingNewTicketToList} />;
buttonText = "Return to Ticket List";
} else {
currentlyVisibleState = <TicketList ticketList={mainTicketList} />;
buttonText = "Add Ticket";
}
// existing code...
We pass handleAddingNewTicketToList as a prop called onNewTicketCreation. Notice the naming convention: handle prefix for the function, on prefix for the prop.
Next, we need to update NewTicketForm to accept and use this prop:
import { type SubmitEvent } from 'react';
import { type TicketData } from '../types';
type NewTicketFormProps = {
onNewTicketCreation: (ticket: TicketData) => void;
};
function NewTicketForm({ onNewTicketCreation }: NewTicketFormProps) {
// We'll update this function in the next step
function handleNewTicketFormSubmission(event: SubmitEvent<HTMLFormElement>) {
event.preventDefault();
const formData = new FormData(event.currentTarget);
console.log(formData.get('names'));
console.log(formData.get('section'));
console.log(formData.get('issue'));
}
return (
<>
<form onSubmit={handleNewTicketFormSubmission}>
<input
type='text'
name='names'
placeholder='Pair Names' />
<input
type='text'
name='section'
placeholder='Section' />
<textarea
name='issue'
placeholder='Describe your issue.' />
<button type='submit'>Help!</button>
</form>
</>
);
}
export default NewTicketForm;
We've added two things:
- We defined a
NewTicketFormPropstype. Our one prop,onNewTicketCreation, is a function that takes aTicketDataand returns nothing, so its type is(ticket: TicketData) => void. This describes exactly what kind of functionNewTicketFormexpects to receive. - We destructured
onNewTicketCreationout of the props in the function signature so we can call it directly.
Step 4: Use the Callback and Add a Unique ID
We're almost done! We need to:
- Generate a unique ID for each new ticket using
crypto.randomUUID(). - Update
handleNewTicketFormSubmissionto create a ticket object and pass it toonNewTicketCreation.
Here's the complete updated NewTicketForm:
import { type SubmitEvent } from 'react';
import { type TicketData } from '../types';
type NewTicketFormProps = {
onNewTicketCreation: (ticket: TicketData) => void;
};
function NewTicketForm({ onNewTicketCreation }: NewTicketFormProps) {
function handleNewTicketFormSubmission(event: SubmitEvent<HTMLFormElement>) {
event.preventDefault();
const formData = new FormData(event.currentTarget);
onNewTicketCreation({
names: formData.get('names') as string,
section: formData.get('section') as string,
issue: formData.get('issue') as string,
id: crypto.randomUUID()
});
}
return (
<>
<form onSubmit={handleNewTicketFormSubmission}>
<input
type='text'
name='names'
placeholder='Pair Names' />
<input
type='text'
name='section'
placeholder='Section' />
<textarea
name='issue'
placeholder='Describe your issue.' />
<button type='submit'>Help!</button>
</form>
</>
);
}
export default NewTicketForm;
We call onNewTicketCreation() and pass in an object with all of the ticket properties, including a unique ID generated by crypto.randomUUID().
formData.get() can return more than just a string (for example, it returns null if no field with that name exists), so TypeScript won't assume the values are strings on its own. Because we know these fields exist and hold text, we use as string to tell TypeScript to treat each value as a string. This is called a type assertion.
If you need to get a number from a form, remember to parse the value. For example:
onNewTicketCreation({
// ...other properties
numberOfStudents: parseInt(formData.get('numberOfStudents') as string)
});
How It All Connects
Let's trace the data flow:
- User fills out the form and clicks "Help!"
handleNewTicketFormSubmissioninNewTicketFormis called- This function builds a ticket object (including a unique ID from
crypto.randomUUID()) and callsonNewTicketCreation()with it onNewTicketCreationis actuallyhandleAddingNewTicketToListfromTicketControlhandleAddingNewTicketToListadds the ticket to state and hides the form- React re-renders
TicketControl, which now passes the updated list toTicketList - The new ticket appears in the queue!
Try it out in the browser. Now when we add a ticket via the form, it will be added to the queue!
Summary
In this lesson, we learned how to pass data from a child component up to a parent component using callbacks. This is a fundamental pattern in React:
- State lives in the parent component that needs to share it.
- We pass a callback function down to child components as a prop.
- The child calls this function (usually in response to an event) and passes data as an argument.
- The parent's function receives the data and updates state.
- Naming convention:
handleXfor the function,onXfor the prop.
This pattern maintains unidirectional data flow while still allowing child components to communicate with their parents.