This post is part of the series 30 Days of React.

In this series, we're starting from the very basics and walk through everything you need to know to get started with React. If you've ever wanted to learn React, this is the place to start!

Repeating Elements

Edit this page on Github

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>)

ReactDOM.render(<div>
  <ShowA />
  <MultipleA />
</div>, document.getElementById('root'));

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];

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 App = (props) => {
  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 of i starting as 1 and then it will run it again for the second value where i will be set as 10 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';
import './App.css';

const a = [1, 10, 100, 1000];
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 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:

import React from 'react';
import './App.css';

const a = [1, 10, 100, 1000];
const App = (props) => {
  return (
    <ul>
      {a.map(i => {
        return <li key={i}>{i}</li>
      })}
    </ul>
  )
}

export default App;

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.

export 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):

export const Formatter = ({format, state}) => {
  let children = 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, state))}
      </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 the React.createElement() function where the arguments are:

  1. The ReactElement we want to clone
  2. Any props we want to add to the instance
  3. 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 the state 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!


Ari Lerner

Hi, I'm Ari. I'm the author of ng-book and I've been teaching Web Development for a long time. I like to speak at conferences and eat spicy food. I technically got paid while I traveled the country as a professional comedian, but have come to terms with the fact that I am not funny.

Connect with Ari on Twitter at @auser.