Course: Section

Get Started with Modern React: Step by Step

Episode: Title

S02・V07: Extracting Components

Date Created: July 17th, 2019
Last Updated: 27 days ago

Objectives
  1. We will split larger components into smaller components, to improve component reusability.
  2. We will show that renaming props can help decouple an extracted component.
Watch Video
Duration: 8m 26s

Setup

Our starting point is as follows (the App component has not been defined yet):

import React from "react";
import ReactDOM from "react-dom";

ReactDOM.render(<App />, document.getElementById("root"));

Let’s say we have some data representing a “comment” on a blog. This comment has a date field, a text field, and an author field, which is an object consisting of name and avatarUrl fields.

import React from "react";
import ReactDOM from "react-dom";

const comment = {  date: new Date(),  text: "I hope you enjoy learning React!",  author: {    name: "Hello Kitty",    avatarUrl: "https://placekitten.com/g/64/64"  }};
ReactDOM.render(<App />, document.getElementById("root"));

Let’s also create a small utility function that takes a date, and returns a localized string representing the date.

function formatDate(date) {
  return date.toLocaleDateString();
}

Adding a ‘Comment’ Component

Now, we can set up a Comment function component. The Comment component will return an outer div. The div will receive a CSS class attribute. As we are writing JSX and not HTML, the CSS class attribute becomes a JSX className prop, written in camel-case.

We will pass the className prop the string “Comment”. Within this outer div, we will put three other div tags. The first will have the className of UserInfo. The second will have the className of Comment-text. And the third will have the className of Comment-date.

import React from "react";
import ReactDOM from "react-dom";

const comment = {
  date: new Date(),
  text: "I hope you enjoy learning React!",
  author: {
    name: "Hello Kitty",
    avatarUrl: "https://placekitten.com/g/64/64"
  }
};

function formatDate(date) {
  return date.toLocaleDateString();
}

function Comment(props) {  return (    <div className="Comment">      <div className="UserInfo"></div>      <div className="Comment-text"></div>      <div className="Comment-date"></div>    </div>  );}
ReactDOM.render(<App />, document.getElementById("root"));

Adding a Top-Level ‘App’ Component

Before we add more to the Comment component, let’s first add our top-level App component. It will return the Comment component. We will pass in three props to the returned Comment component, which will get their values from the comment data object. First, the date prop. Second, the text prop. Third, the author prop. Now, the Comment component has a props object which contains date, text, and author data passed down into it.

import React from "react";
import ReactDOM from "react-dom";

const comment = {
  date: new Date(),
  text: "I hope you enjoy learning React!",
  author: {
    name: "Hello Kitty",
    avatarUrl: "https://placekitten.com/g/64/64"
  }
};

function formatDate(date) {
  return date.toLocaleDateString();
}

function Comment(props) {
  return (
    <div className="Comment">
      <div className="UserInfo"></div>
      <div className="Comment-text"></div>
      <div className="Comment-date"></div>
    </div>
  );
}

function App() {  return (    <Comment      date={comment.date}      text={comment.text}      author={comment.author}    />  );}
ReactDOM.render(<App />, document.getElementById("root"));

We can demonstrate this by adding a console.log() of the props object to the Comment component before the return expression.

function Comment(props) {
  console.log('props:', props);  return (
    <div className="Comment">
      <div className="UserInfo"></div>
      <div className="Comment-text"></div>
      <div className="Comment-date"></div>
    </div>
  );
}

We can now open the JavaScript Console, and then reload the page to see the printed object.

We will use the JSON.stringify() method to pretty-print the props object.

function Comment(props) {
  console.log('props:', JSON.stringify(props, null, 2));  return (
    <div className="Comment">
      <div className="UserInfo"></div>
      <div className="Comment-text"></div>
      <div className="Comment-date"></div>
    </div>
  );
}

You can see that the props object has date, text, and author information within it.

Adding an Image

Now, within the UserInfo div, we will add in img. Within the img, we will add a className of Avatar. imgs need a src attribute, so we will add a src prop, which gets its data from the props object passed in to the component. And, each img should have an alt attribute, to which we will pass in the author’s name.

function Comment(props) {
  console.log('props:', JSON.stringify(props, null, 2));
  return (
    <div className="Comment">
      <div className="UserInfo">
        <img          className="Avatar"          src={props.author.avatarUrl}          alt={props.author.name}        />      </div>
      <div className="Comment-text"></div>
      <div className="Comment-date"></div>
    </div>
  );
}

The image shows on our page.

Underneath the image, we will create another div, with className of UserInfo-name. Inside this div, we will show the author’s name. The author’s name now shows on our page. Now, we will add the comment text. The comment text now shows on our page. Finally, we will add the comment date, using the formatDate() utility function.

function Comment(props) {
  console.log('props:', JSON.stringify(props, null, 2));
  return (
    <div className="Comment">
      <div className="UserInfo">
        <img
          className="Avatar"
          src={props.author.avatarUrl}
          alt={props.author.name}
        />
        <div className="UserInfo-name">
          {props.author.name}        </div>
      </div>
      <div className="Comment-text">
        {props.text}      </div>
      <div className="Comment-date">
        {formatDate(props.date)}      </div>
    </div>
  );
}

The comment date now shows on our page. The Comment component is now complete.

Extracting ‘Avatar’ Component

However, the Comment component can be broken up into smaller and simpler components. First, let’s extract a component that will show the avatar image. We will call the component Avatar. Now, we will move the img from the Comment component into the Avatar component.

function Avatar(props) {
  return (
    <img
      className="Avatar"
      src={props.author.avatarUrl}
      alt={props.author.name}
    />
  );
}

Now, we can refer to the Avatar component in its place, making sure we pass in an author prop.

function Comment(props) {
  console.log('props:', JSON.stringify(props, null, 2));
  return (
    <div className="Comment">
      <div className="UserInfo">
        <Avatar author={props.author} />        <div className="UserInfo-name">
          {props.author.name}
        </div>
      </div>
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

Save the changes, and note that the page is unchanged. We have successfully extracted the Avatar component.

Renaming Props

Note that the Avatar component doesn’t need to know that it is being rendered inside a Comment component, so the prop name author can be renamed to something more generic. We can rename the author prop to user.

function Avatar(props) {
  return (
    <img
      className="Avatar"
      src={props.user.avatarUrl}      alt={props.user.name}    />
  );
}

function Comment(props) {
  console.log('props:', JSON.stringify(props, null, 2));
  return (
    <div className="Comment">
      <div className="UserInfo">
        <Avatar user={props.author} />        <div className="UserInfo-name">
          {props.author.name}
        </div>
      </div>
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

This simple refactoring makes the Avatar component less coupled to the Comment component, and therefore more reusable. We recommend naming props from the component’s own point of view rather than the context in which it is being used.

Extracting ‘UserInfo’ Component

Next, we will extract a UserInfo component that renders an Avatar next to the user’s name. We will move the user info div from the Comment component to the UserInfo component. Let’s not forget to rename author to user within the UserInfo component. Now, we can refer to the UserInfo component in its place, making sure we pass in an user prop.

import React from "react";
import ReactDOM from "react-dom";

const comment = {
  date: new Date(),
  text: "I hope you enjoy learning React!",
  author: {
    name: "Hello Kitty",
    avatarUrl: "https://placekitten.com/g/64/64"
  }
};

function formatDate(date) {
  return date.toLocaleDateString();
}

function Avatar(props) {
  return (
    <img
      className="Avatar"
      src={props.user.avatarUrl}
      alt={props.user.name}
    />
  );
}

function UserInfo(props) {  return (    <div className="UserInfo">      <Avatar user={props.user} />      <div className="UserInfo-name">        {props.user.name}      </div>    </div>  );}
function Comment(props) {
  console.log('props:', JSON.stringify(props, null, 2));
  return (
    <div className="Comment">
      <UserInfo user={props.author} />      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

function App() {
  return (
    <Comment
      date={comment.date}
      text={comment.text}
      author={comment.author}
    />
  );
}

ReactDOM.render(<App />, document.getElementById("root"));

Save the file and make sure the page is unchanged after our refactoring.

Extracting components might seem like grunt work at first, but having a palette of reusable components pays off in larger apps. A good rule of thumb is that if a part of your UI is used several times, or is complex enough on its own, it is a good candidate to be a reusable component.

Summary

We split larger components into smaller components, to improve component reusability. And, we showed that renaming props can help decouple an extracted component.

Next Up…

In the next video, we will look at component state.