📓 4.2.2.4 Passing Data Via Callbacks
In the last lesson, we covered the concept of unidirectional data flow. In this lesson, we'll apply what we've learned. To recap, we'll need to do the following:
- Add
mainTicketListto state in ourTicketControlcomponent. - Create a function in
TicketControlthat will take form data and add it to our ticket list. - Pass this function down to the child
NewTicketFormcomponent as a prop. - Call this function in our child component when 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 in class and on your own.
Step 1: Add mainTicketList to State
Let's start by adding a mainTicketList state variable and passing it down as a prop to TicketList:
import React, { useState } from 'react';
import NewTicketForm from './NewTicketForm';
import TicketList from './TicketList';
function TicketControl() {
const [formVisibleOnPage, setFormVisibleOnPage] = useState(false);
const [mainTicketList, setMainTicketList] = useState([]); // new code
const handleClick = () => {
setFormVisibleOnPage(!formVisibleOnPage);
}
let currentlyVisibleState = null;
let buttonText = null;
if (formVisibleOnPage) {
currentlyVisibleState = <NewTicketForm />;
buttonText = "Return to Ticket List";
} else {
currentlyVisibleState = <TicketList ticketList={mainTicketList} />; // updated
buttonText = "Add Ticket";
}
return (
<React.Fragment>
{currentlyVisibleState}
<button onClick={handleClick}>{buttonText}</button>
</React.Fragment>
);
}
export default TicketControl;
Notice we're initializing mainTicketList as an empty array. We're doing this because we don't want this application to start with fake tickets. The queue should be empty until we start adding tickets via our form. (We'll be removing our array of dummy tickets from TicketList in just a moment.)
Also, notice how we're passing mainTicketList down to TicketList as a prop called ticketList.
Step 2: Update TicketList to Use Props
In step 1, we passed mainTicketList state from TicketControl down to our TicketList component. Now we need to update TicketList.js to use props and add prop types. We'll also remove the mainTicketList constant that holds our dummy tickets.
import React from "react";
import Ticket from "./Ticket";
import PropTypes from "prop-types";
// Remove const mainTicketList = [ ... ]. We no longer need these dummy tickets.
function TicketList(props) {
return (
<React.Fragment>
<hr />
{props.ticketList.map((ticket, index) =>
<Ticket
names={ticket.names}
location={ticket.location}
issue={ticket.issue}
key={index} />
)}
</React.Fragment>
);
}
TicketList.propTypes = {
ticketList: PropTypes.array
};
export default TicketList;
We've made several changes here:
- Now that we are passing
ticketListdown throughprops, we need to importprop-typesand add a prop type ofarrayfor ourticketList. - We removed our
mainTicketListconstant which stored the dummy tickets — we won't need these anymore! - We loop through
props.ticketListinstead of the local constant.
Now we'll be able to make changes to our ticket list and display tickets as they're added.
Step 3: 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 React, { useState } from 'react';
import NewTicketForm from './NewTicketForm';
import TicketList from './TicketList';
function TicketControl() {
const [formVisibleOnPage, setFormVisibleOnPage] = useState(false);
const [mainTicketList, setMainTicketList] = useState([]);
const handleClick = () => {
setFormVisibleOnPage(!formVisibleOnPage);
}
const handleAddingNewTicketToList = (newTicket) => {
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.
Naming convention: 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. -
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 4: Pass the Function Down as a Prop
Now we need to pass handleAddingNewTicketToList down to our NewTicketForm component as a prop:
// Inside TicketControl...
let currentlyVisibleState = null;
let buttonText = null;
if (formVisibleOnPage) {
currentlyVisibleState =
<NewTicketForm
onNewTicketCreation={handleAddingNewTicketToList} />; // updated
buttonText = "Return to Ticket List";
} else {
currentlyVisibleState =
<TicketList
ticketList={mainTicketList} />;
buttonText = "Add Ticket";
}
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 React from "react";
import PropTypes from "prop-types";
function NewTicketForm(props) {
// We'll update this function in the next step
function handleNewTicketFormSubmission(event) {
event.preventDefault();
console.log(event.target.names.value);
console.log(event.target.location.value);
console.log(event.target.issue.value);
}
return (
<React.Fragment>
<form onSubmit={handleNewTicketFormSubmission}>
<input
type='text'
name='names'
placeholder='Pair Names' />
<input
type='text'
name='location'
placeholder='Location' />
<textarea
name='issue'
placeholder='Describe your issue.' />
<button type='submit'>Help!</button>
</form>
</React.Fragment>
);
}
NewTicketForm.propTypes = {
onNewTicketCreation: PropTypes.func
};
export default NewTicketForm;
We've added two things:
- Made sure
propsis a parameter of our function component. - Added
PropTypesforonNewTicketCreation, specifying it's a function.
Step 5: Use the Callback and Add a Unique ID
We're almost done! We need to:
- Import the UUID library to assign unique IDs to new tickets.
- Update
handleNewTicketFormSubmissionto create a ticket object and pass it toonNewTicketCreation.
Here's the complete updated NewTicketForm:
import React from "react";
import PropTypes from "prop-types";
import { v4 } from 'uuid';
function NewTicketForm(props) {
function handleNewTicketFormSubmission(event) {
event.preventDefault();
props.onNewTicketCreation({
names: event.target.names.value,
location: event.target.location.value,
issue: event.target.issue.value,
id: v4()
});
}
return (
<React.Fragment>
<form onSubmit={handleNewTicketFormSubmission}>
<input
type='text'
name='names'
placeholder='Pair Names' />
<input
type='text'
name='location'
placeholder='Location' />
<textarea
name='issue'
placeholder='Describe your issue.' />
<button type='submit'>Help!</button>
</form>
</React.Fragment>
);
}
NewTicketForm.propTypes = {
onNewTicketCreation: PropTypes.func
};
export default NewTicketForm;
We call props.onNewTicketCreation() and pass in an object with all of the ticket properties, including a unique ID generated by the UUID library.
If you need to get a number from a form, remember to parse the value. For example:
props.onNewTicketCreation({
// ...other properties
numberOfStudents: parseInt(event.target.numberOfStudents.value)
});
How It All Connects
Let's trace the data flow:
- User fills out the form and clicks "Help!"
handleNewTicketFormSubmissioninNewTicketFormis called- This function calls
props.onNewTicketCreation()with the ticket data 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!
Complete TicketControl Component
Here's the complete TicketControl component for reference:
import React, { useState } from 'react';
import NewTicketForm from './NewTicketForm';
import TicketList from './TicketList';
function TicketControl() {
const [formVisibleOnPage, setFormVisibleOnPage] = useState(false);
const [mainTicketList, setMainTicketList] = useState([]);
const handleClick = () => {
setFormVisibleOnPage(!formVisibleOnPage);
}
const handleAddingNewTicketToList = (newTicket) => {
const newMainTicketList = mainTicketList.concat(newTicket);
setMainTicketList(newMainTicketList);
setFormVisibleOnPage(false);
}
let currentlyVisibleState = null;
let buttonText = null;
if (formVisibleOnPage) {
currentlyVisibleState =
<NewTicketForm
onNewTicketCreation={handleAddingNewTicketToList} />;
buttonText = "Return to Ticket List";
} else {
currentlyVisibleState =
<TicketList
ticketList={mainTicketList} />;
buttonText = "Add Ticket";
}
return (
<React.Fragment>
{currentlyVisibleState}
<button onClick={handleClick}>{buttonText}</button>
</React.Fragment>
);
}
export default TicketControl;
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.