Today we're going to work through how to display multiple components in preparation for pulling in external data into our app.
Up through this point, we've been building a basic application without any external data. Before we get there (we'll start on this functionality tomorrow), let's look over something we glossed over in the previous two weeks:
Repeating elements
We've already seen this before where we've iterated over a list of objects and render multiple components on screen. Before we add too much complexity in our app with loading external data, today we'll take a quick peek at how to repeat components/elements in our app.
Since JSX is seen as plain JavaScript by the browser, we can use any ole' JavaScript inside the template tags in JSX. We've already seen this in action. As a quick demo:
const a = 10;
const ShowA = () => <div>{a}</div>;
const MultipleA = () => <div>{a * a}</div>;
const App = props => {
return (
<div className="app">
<ShowA />
<MultipleA />
</div>
);
};
Notice the things inside of the template tags {}
look like simple JavaScript. That's because it is just JavaScript. This feature allows us to use (most) native features of JavaScript inside our template tags including native iterators, such as map
and forEach
.
Let's see what we mean here. Let's convert the previous example's a
value from a single integer to a list of integers:
const a = [1, 10, 100, 1000, 10000];
We can map over the a
variable here inside our components and return a list of React components that will build the virtual DOM for us.
const a = [1, 10, 100, 1000, 10000];
const Repeater = () => {
return (
<ul>
{a.map(i => {
return <li>{i}</li>;
})}
</ul>
);
};
What is the
map()
function?The
map
function is a native JavaScript built-in function on the array. It accepts a function to be run on each element of the array, so the function above will be run four times with the value ofi
starting as1
and then it will run it again for the second value wherei
will be set as10
and so on and so forth.
Let's update the app we created on day 12 with our App
component here. Let's open up our src/App.js
file and replace the content of the App
component with this source. Cleaning up a few unused variables and your src/App.js
should look similar to this:
import React from "react";
const a = [1, 10, 100, 1000, 10000];
const App = props => {
return (
<ul>
{a.map(i => {
return <li>{i}</li>;
})}
</ul>
);
};
export default App;
Starting the app again with the command generated by the create-react-app
command: npm start
, we can see the app is working in the browser!
However, if we open the developer console, we'll see we have an error printed out. This error is caused by the fact that React doesn't know how to keep track of the individual components in our list as each one just looks like a <li />
component.
For performance reasons, React uses the virtual DOM to attempt to limit the number of DOM elements that need to be updated when it rerenders the view. That is if nothing has changed, React won't make the browser update anything to save on work.
This feature is really fantastic for building web applications, but sometimes we have to help React out by providing unique identifiers for nodes. Mapping over a list and rendering components in the map is one of those times.
React expects us to uniquely identify components by using a special prop: the key
prop for each element of the list. The key
prop can be anything we want, but it must be unique for that element. In our example, we can use the i
variable in the map as no other element in the array has the same value.
Let's update our mapping to set the key:
const App = props => {
return (
<ul>
{a.map(i => {
return <li key={i}>{i}</li>;
})}
</ul>
);
};
Children
We talked about building a parent-child relationship a bit earlier this week, but let's dive a bit more into detail about how we get access to the children inside a parent component and how we can render them.
On day 11, we built a <Formatter />
component to handle date formatting within the Clock component to give our users flexibility with their own custom clock rendering. Recall that the implementation we created is actually pretty ugly and relatively complex.
const Formatter = props => {
let children = props.format.split("").map((e, idx) => {
if (e === "h") {
return <Hour key={idx} {...props} />;
} else if (e === "m") {
return <Minute key={idx} {...props} />;
} else if (e === "s") {
return <Second key={idx} {...props} />;
} else if (e === "p") {
return <Ampm key={idx} {...props} />;
} else if (e === " ") {
return <span key={idx}> </span>;
} else {
return <Separator key={idx} {...props} />;
}
});
return <span>{children}</span>;
};
We can use the React.Children
object to map over a list of React objects and let React do this heavy-lifting. The result of this is a much cleaner Formatter
component (not perfect, but functional):
const Formatter = props => {
let children = props.format.split("").map(e => {
if (e == "h") {
return <Hour />;
} else if (e == "m") {
return <Minute />;
} else if (e == "s") {
return <Second />;
} else if (e == "p") {
return <Ampm />;
} else if (e == " ") {
return <span> </span>;
} else {
return <Separator />;
}
});
return (
<span>
{React.Children.map(children, c => React.cloneElement(c, props))}
</span>
);
};
React.cloneElement
We have yet to talk about the
React.cloneElement()
function, so let's look at it briefly here. Remember WWWWWAAAAAYYYYY back on day 2 we looked at how the browser sees JSX? It turns it into JavaScript that looks similar to:React.createElement("div", null, React.createElement("img", {src: "profile.jpg", alt: "Profile photo"}), React.createElement("h1", null, "Welcome back Ari") );
Rather than creating a new component instance (if we already have one), sometimes we'll want to copy it or add custom props/children to the component so we can retain the same props it was created with. We can use
React.cloneElement()
to handle this for us.The
React.cloneElement()
has the same API as theReact.createElement()
function where the arguments are:
- The ReactElement we want to clone
- Any
props
we want to add to the instance- Any
children
we want it to have.In our
Formatter
example, we're creating a copy of all the children in the list (the<Hour />
,<Minute />
, etc. components) and passing them theprops
object as their props.
The React.Children
object provides some nice utility functions for dealing with children. Our Formatter
example above uses the map
function to iterate through the children and clone each one in the list. It creates a key
(if necessary) for each one, freeing us from having to manage the uniqueness ourselves.
Let's use the React.Children.map()
function to update our App component:
const App = props => {
return (
<ul>
{React.Children.map(a, i => (
<li>{i}</li>
))}
</ul>
);
};
Back in the browser, everything still works.
There are several other really useful methods in the React.Children
object available to us. We'll mostly use the React.Children.map()
function, but it's good to know about the other ones available to us. Check out the documentation for a longer list.
Up through this point, we've only dealt with local data, not really focusing on remote data (although we did briefly mention it when building our activity feed component). Tomorrow we're going to get into interacting with a server so we can use it in our React apps.
Great work today!