Passing data with properties
In the last section, you updated your App
component to show off a list of
hardcoded to-do items. This component now feels like it does something. To
bring it to the next level, it's time to start passing data around.
You will build out two new components, a List
and a ListItem
. The list will
hold all of the to-dos, and the item will be responsible for displaying the
details about any single to-do. This will allow both to focus on their core
responsibilities and pass the necessary data to the children.
Passing data down
In React you will often pass data down to components that are within your current component. These sub-components are called children. These children, in turn, may have more children. This continues until you reach a component that has no further children. That's where the chain stops.
React uses this component tree to logically pass information down. Each component in the tree should only pass only the required information along to the children.
For your app, the List
will hold all the to-dos, and pass only the single item
to the ListItem
The ListItem, for now, won't have any children, so the chain
will stop there.
Passing data with properties
To pass data between components, React uses a syntax that works similarly to HTML.
const ComponentA = (props: any) => {return <div>{props.name}</div>}const ComponentB = (props: any) => {return <ComponentA name="Ethan" />}
Every component in React accepts a single parameter. This can be named anything,
but by convention, it's named props
, which is short for properties.
props
value is a plain old JavaScript object. In TypeScript it can be given a
type. This object has all the key-value pairs that are passed to it.
Every time you pass a parameter you use the key=value
syntax. This is true for
native HTML elements like <input type="text" />
or custom components like
above.
This works a lot better than having multiple function parameters. HTML elements can have a lot of different parameters and many of them are optional. Imagine if you had to list them out:
function img(src, alt, title, style, className, height, width, ...) {}
This breaks down for custom components since you could pass any number of parameters. Instead of listing things out, React passes properties in an object.
The child component (the one being called), can reference all of these key-value
pairs by looking up the key
on the props
object. This can be done with dot
notation, props.key
or bracket notation, props["key"]
.
Properties are read-only.
A very important core tenant of React is that the data passed into a component is immutable.
This includes things like strings and numbers, but also more complex data like objects and arrays. You can make a copy of these values and change those, but the actual values passed in should never be changed by your component. Props are immutable.
With types
Because you're coding this with TypeScript, you can effortlessly add type
checking to your components. Instead of using any
like above, you can define
and use a type:
type Props = {name: string}const ComponentA = (props: Props) => {return <div>{props.name}</div>}const ComponentB = (props: any) => {return <ComponentA name="Ethan" />}
Above, ComponentA
defines the type for its props
. By convention, if there is
no need for a different name, the type name should be Props
. Therefore, when
ComponentB
calls it below, if it does not pass a name
that is a string, you
will get a compile-time error. This allows you to code faster and write more
correct code. You can use TypeScript's optional types to allow for powerful and
flexible types.
With destructuring
Lastly, when referencing the properties you want, it can be tiresome to
reference props.key
each time. JavaScript offers an elegant solution to
destructure the parameters in the function definition. The syntax is:
({ key1, key2 }: TypeDefinition)
So the above example changes to:
type Props = {name: string}const ComponentA = ({ name }: Props) => {return <div>{name}</div>}
This takes the props
and destructures them into the named key-value pairs.
In this case, the props object will take prop.name
and put the value into the
name
variable in the function definition. This allows you to reference the
value you want with just the short name.
You can read more about destructuring on MDN.
Building real components
Now that you know how to pass data down to your components, you can build out some smarter components for your to-do app.
The two responsible components in your app are the to-do list itself, and each item. As these grow with complexity it will be good to have them as separate entities.
To-Do List
It's often easier to think from the top of the tree to the bottom. That is, start with the big components and work your way through their children. If you know all the components you need upfront, you can also start with the smallest and work your way up.
To just get going, you'll start with the list itself. You can take your App
component and copy it as the base:
const TodoList = () => (<ul>{todos.map((todo) => (<li>{todo.text}</li>))}</ul>)
This just copies the App
and renames it. Since you moved the logic out of
App
, go ahead and just have App
call your new component:
const App = () => <TodoList />
Great! Although the new component though is using a globally scoped todos
reference. While that works, it's not ideal. A component can use global
constants, but you want this list to change eventually. That todos
array was
just convenience, and you're beyond that now. No, instead, you should pass the
to-dos into this component as a property!
Add a TypeScript definition for the props you expect. In this case, a list of
Todos
.
interface TodoListProps {todos: Todo[]}
Remember, even though you are only interested in a single property, the props
are passed as an object. You cannot get a single property from them. Here, you
are defining the object passed should have a single key, todos
, with a value
that is an array of Todo
.
Add the definition to the list:
const TodoList = ({ todos }: TodoListProps) => (...)
And when you do that, you should have a TypeScript compiler error! The App
component is not passing this new property yet. The default property object
passed to a component is an empty object, {}
so the error looks like this:
Property 'todos' is missing in type '{}' but required in type 'TodoListProps'. ts(2741)
Alright, fair enough. We declared todos
to be required in the interface and
didn't pass it. Update the App component to pass it:
const App = () => <TodoList todos={todos} />
With all this moving around of code, your app should look blissfully the same.
Adding the TodoListItem
The TodoList
above doesn't use a custom component, it just creates an li
HTML element. You have big plans for this app, so a custom component is
necessary. It's time to create the TodoListItem
. This item will be responsible
for displaying the <li>
and the text. What data is going to be passed to this?
Just the text
of a Todo. The good news is you already have an interface that
defines that exact thing, the Todo
interface itself!
You can reuse that and create:
const TodoListItem = ({ text }: Todo) => <li>{text}</li>
This single line component takes in text
and displays it in an li
. You
aren't calling it yet though, so return to your TodoList
component and update
it.
Instead of:
<ul>{todos.map((todo) => (<li>{todo.text}</li>))}</ul>
Replace it with:
<ul>{todos.map(({ text }, index) => (<TodoListItem key={index} text={text} />))}</ul>
Boom! This destructures the todo
in the map
function call since there is
just a single key to work with. It also pulls in the index
param to use as the
key
below. It calls the new TodoListItem
component and passes key
and
text
to the item.
So uh, what the heck is key
.
Special props, key
, and children
.
React has two magical properties. The first is key
. The official docs explain
it well:
Keys help React identify which items [in a list] have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity.
The best way to pick a key is to use a string that uniquely identifies a list item among its siblings. Most often you would use IDs from your data as keys. When you don't have stable IDs for rendered items, you may use the item index as a key as a last resort:
When creating any elements from a list you must give those elements a unique
key.
Here, you don't have anything other than the index to use. Any to-do may have the same text as another to-do. Since they are not unique, the index will have to do for now.
Children
The other special property is children
. This is used to access children
elements that are passed to a component. For example, imagine a component whose
responsibility is to be a card.
const Card => ({ children }) => (<div className="box-shadow rounded-corners background-white">{children}</div>)
This component wants to take any content and wrap it in a card like diagram. It doesn't accept those children as regular props, the children are the elements inside the card. Called like this:
<Card><div><h1>Title</h1><p>lots of text</p></div></Card>
To refer to the div
and elements passed into the card, the special name
children
is used. The Card
component then just “passes through” the children
and renders them using {children}
.
This does bring up an interesting point of “what's correct?” Both of these are indentical:
// Option Aconst TodoListItem = ({ text }: Todo) => <li>{text}</li>// Option Bconst TodoListItem = ({ children }: Todo) => <li>{children}</li>
However they are called differently:
// Option A<TodoListItem text={text} />// Optionn B<TodoListItem>{text}</TodoListItem>
Which is correct? It mostly depends on the situation and the contents of text.
If text
ends up being a lot of DOM and other components, it's acting more like
part of the DOM tree than just a string. If text
is a simple string, then
passing it as a property named text
is simple.
Wrapping up
You have refactored your to-do application to use more components. These components are separated by responsibility. They don't do a ton yet, but there is room for growth. Importantly these components can be reused and they separate concerns.
You can now pass data down through your application. This lets you pass the to-dos down to the list item, which displays the information.
Of course, it's still a static list. You can't change anything. While properties
are read-only and immutable, state is not. And to change up your list, you
will need to remove the constant todos
and step into the world of managing
state, something that changes:
const [todos, setTodos] = useState<Todo[]>([])
This will turn your demo into a fully functioning app. Ready to learn how?