Skip to main content
Version: v1.4

📓 3.3.0.9 Props

In this lesson, we'll learn how to pass data between React components. For now, this data will be hard-coded, not dynamic, because we aren't quite ready to work with state yet.

Our Ticket component currently looks like this:

src/components/Ticket.tsx
function Ticket(){
const name = "Thato";
const name2 = "Haley";
return (
<>
<h3>3a</h3>
<h3>{name} and {name2}</h3>
<p><em>React component not rendering!</em></p>
<hr/>
</>
);
}

export default Ticket;

Right now the ticket data is hard-coded inside Ticket.tsx itself. That might be fine for a single ticket, but it falls apart the moment we want more than one: each ticket would need its own copy of hard-coded data baked directly into the component, which doesn't scale and isn't how real apps work. Instead, we'll move the ticket data up into a parent component (TicketList) and pass it down to each Ticket.tsx through props. The parent owns the data; Ticket.tsx just displays whatever it receives. This sets us up for working with state later on. When we replace this hard-coded data with real, dynamic state, we'll want that state managed in one place: a single source of truth. If every Ticket.tsx managed its own data, keeping that data updated and in sync across tickets would quickly become a mess. With one parent holding all the ticket data and passing it down, we have exactly one place to make changes, and every ticket reflects those changes automatically.

React components accept properties (known as props) passed down from a parent. Because React components are functions, these props are actually just a special kind of argument.

Passing Props

We can pass props down to a child component using JSX tags. Let's update the TicketList.tsx component so it can pass props to its child Ticket.tsx component:

src/components/TicketList.tsx
import Ticket from "./Ticket";

function TicketList(){
return (
<Ticket
section="3A"
names="Thato and Haley"
issue="React component not rendering!"
/>
);
}

export default TicketList;

We've added three props here: section, names and issue. It's common to put each prop on a separate line. While this isn't required, it makes it easier to read our code so we can see what's being passed down to child components.

Accessing Props

Next, let's update our Ticket so that it uses the props from its parent TicketList component.

src/components/Ticket.tsx
type TicketProps = {
section: string;
names: string;
issue: string;
}

function Ticket({ section, names, issue }: TicketProps){
return (
<>
<h3>{section} - {names}</h3>
<p><em>{issue}</em></p>
<hr/>
</>
);
}

export default Ticket;

There's a lot happening here, so let's break it down. First, we define a type called TicketProps that lists each prop the Ticket component expects along with its type. This is the same type alias syntax we used throughout the TypeScript section, and it works exactly the same way here. Each property in the type corresponds to a single prop the component accepts, and the property name must match the name the parent passes down. Notice that section, names, and issue line up precisely with the three props we wrote in TicketList. The type annotation on each property tells TypeScript what kind of value to expect, so if a parent ever passes the wrong type, such as a number where we expect a string, TypeScript will catch the mismatch for us.

It's also worth noticing that every property in TicketProps is required by default. Because we listed section, names, and issue with no special markers, TypeScript expects all three of them every time we render a Ticket. If a parent renders <Ticket /> without passing all three, TypeScript will flag it as an error right away, while we're still writing the code, rather than letting the problem slip through and surface later when the app runs. This is one of the biggest advantages of typing our props: we find out about missing or mismatched data immediately.

In this case all three of our props are strings, but props can be any TypeScript type we already know. A prop could just as easily be a number, a boolean, an array, or any other type we covered in the TypeScript section. The only rule is that the type we declare here has to match the value the parent actually passes. And just like in the TypeScript section, if we ever want a prop to be optional, we can mark it with a ? after its name, and TypeScript will no longer require the parent to provide it.

Next, we use parameter destructuring in the function signature: function Ticket({ section, names, issue }: TicketProps). Destructuring is a JavaScript feature you've seen before, and TypeScript lets us attach a type annotation directly to it. This does two things at once. It pulls each prop out by name so we can reference it directly, and the : TicketProps annotation tells TypeScript that the props passed into this component must match the shape of our TicketProps type. If a parent ever tries to render a Ticket without one of these props, or with the wrong type, TypeScript will warn us before we ever run the code.

Remember that our components are just functions, and props are simply the argument passed into that function. The TicketProps type describes the shape of that argument, and destructuring lets us name each piece of it up front.

As always, JSX JavaScript expressions must be wrapped in curly braces. Content inside curly braces will be evaluated instead of literally rendered. Because we destructured our props in the function signature, we can reference them directly by name, such as section and names, instead of writing props.section and props.names.

Props Are Read-Only

React components aren't just functions; they are pure functions. As we know from our functional programming course section, pure functions don't have side effects and don't alter state.

We need to follow these same rules when we are working with props. We will never alter the value of props because this would alter the state of our application and break a cardinal rule of pure functions: no side effects.

For that reason, it's very important to remember that props are read-only.

The Power of Props

So what is the point of doing this? We're lifting data out of the Ticket component and passing it down from TicketList, but isn't this just an extra step? Well, let's add a second Ticket to TicketList and see.

src/components/TicketList.tsx
import Ticket from "./Ticket";

function TicketList(){
return (
<>
<Ticket
section="3A"
names="Thato and Haley"
issue="React component not rendering!"
/>
<Ticket
section="4B"
names="Sleater and Kinney"
issue="TypeScript error in component."
/>
</>
);
}

export default TicketList;

All we've done here is add a second ticket, this time with different props. The magic of this is what we didn't have to write all of the code for another Ticket. In the next lesson, we'll learn how to leverage looping to make this process even DRYer so we can create any number of Tickets.