React Components

Chapter 10 37 mins

Learning outcomes:

  1. What are components
  2. Creating and instantiating components
  3. Working with props
  4. What is prop drilling
  5. Using the children prop
  6. The defacto App component
  7. Separate components, separate files

Introduction

After elements in React, perhaps the next most important concept is that of components. In effect, all elements in React originate from these so-called components.

In this chapter, we shall unravel all the theoretical and practical details of working with components in React, starting with a detailed discussion of what exactly a component is.

We'll see the two categories of components in React, host components and composite components, and focus the entire chapter on the latter. And specifically, for this category, we'll consider function components (instead of the relatively older class components).

What are components?

If we look from the perspective of an application:

A component is a reusable piece of the UI.

It's not very difficult to think of a component in this way.

If you've ever spent some time designing a website's wireframe, even on a piece of paper, you might've already used the idea of components without realizing about it.

For instance, let's suppose you're sketching the wireframe of a simple blog site. Like all sites, you place a header at the very top of the site.

This header is a component. It's a reusable part of the UI, although you won't be using the header elsewhere (a site has only one header, right?).

Needless to say, a component can itself be made up of other components.

Going back to the same header example above, you might've drawn a box representing the logo and another box representing the navigation bar inside the header. These boxes denote further components, i.e. a logo component and a nav component. The logo component might be used inside the footer of the blog as well — an instance of component reuse.

When designing a website, web app, or just about any user interface, we almost always think in terms of these discrete pieces of the UI. They are very natural to our thought process of designing the interface.

React takes this idea a step further and proposes to think in terms of components when programming a web app as well.

This has the benefit that as soon as the design stage completes, we already have a pretty decent overview of what components to create in React.

Anyways, let's now see a component from a technical perspective:

A component in React is a function that returns a React node.

As stated here, a component precisely refers to a function, a construct, that returns back a React node to be rendered somewhere in an app.

For consistency, we'll stick to this stance of a component referring to a construct returning a React node.

However, it's worthwhile noting that this 'component' term is overloaded out there in the community. Sometimes, component is meant to refer not just to the function but also to an element instance of that function.

Honestly, this ain't completely wrong — after all, in design speak, we might often use the term 'component' to refer to actual, concrete bits and pieces of the UI. But, if we want to be square enough in our usage of terminology, we should try to stick to one meaning, while also knowing of the alternate usage.

Anyways, before we move on, another impotant thing to clarify is the meaning of host components and composite components.

Composite components vs. host components

If we look up into the internals, the source code of React, and their documentation, we see that there is a usage of the following two terms: host components vs. composite components.

So what are these terms?

First off, keep in mind that 'components' here refer to actual elements, not to any kind of functions. We already discussed this overloaded nature of the term 'components' above.

Host components, also known as built-in components, are simply components that map one-to-one to the actual bits and pieces of the host where React renders stuff.

For example, the div component in React (which we can also refer to as the <div> component) is a host component that maps one-to-one to the <div> element provided by HTML.

On the other side of the mirror, composite components, also known as user-defined components, are components that we (the users) define ourselves, and that are composed of other components.

For example, the App component in React (which we can also refer to as the <App> component) is a composite component which might be composed of other components, such as <div>, <Greeting>, <button>, etc.

Once again, because these terms use 'component' in a different way, we'll refrain from using them too much in this course. Whenever we'll refer to a component, we'll be talking about a function; whenever we'll refer to a component instance, we'll be talking about an actual element instantiated from that component.

Creating and instantiating components

Creating a component simply boils down to creating a function, whose name is capitalized and that returns back a React node.

We've already seen quite a few examples of components in the previous chapters but let's do that once more. A quick review, what do say?

In the following code, we define a Greeting component:

function Greeting() {
   return (
      <h1>Hello World!</h1>
   );
}

Absolutely simple.

Now, it's crucial to know that a component on its own doesn't denote anything concrete in the user interface. It is merely a blueprint for an actual piece of the UI.

This is easy to think in terms of a class in OOP. A class, analogous to a component here, is merely a blueprint for an object; nothing actual exists in a program just by creating the class. A concrete instance of the class is only created when it is instantiated.

This instantiation of a component is done by creating a React element using it. And that's done by calling createElement(), with its first argument being the component.

Thereafter, this created element is referred to as the component's instance.

Creating a component vs. a component instance

Let's reiterate on the distinction between creating a component and creating a component instance.

A component is created by constructing a function JavaScript, whereas a component instance is created by invoking createElement() and passing in the component (the function) as the first argument.

From this point onwards, we'll refer to a component to mean a function, and a component instance to mean an element creating using the component.

A component can be instantiated as many times as we like to.

Alright, so let's now instantiate our Greeting component, using JSX:

function Greeting() {
   return (
      <h1>Hello World!</h1>
   );
}

const element = <Greeting/>;

As we already know by now, <Greeting/> gets converted to a createElement() call and thus serves the instantiation process:

React.createElement(Greeting)

Now, as soon as this code is run, we have a living Greeting component instance rendered inside the #root element, all thanks to <Greeting/>.

So what do you say, wasn't it easy to instantiate a component?

Working with props

When components are used, they are sparingly used without props.

As we learnt in the React Basics chapter, props are simply analogous to attributes in HTML — they serve to define additional information for given elements.

Recall that, when creating a React element, all the props are provided in the second argument to creatElement(). Or in the case of a JSX element, the props are provided as attributes of the JSX element.

Now, when the element here instantiates a component, all the given props are handed over to the component function, in its first argument.

Let's see an example using our Greeting component defined above.

Suppose we want to allow a user to provide a <Greeting> element with a given name, in which case the rendered <h1> would read that name.

Since this is additional information related to the element, we need a prop on the component. Let's call this prop name.

Here's how we'd use it:

function Greeting(props) {
   return (
      <h1>Hello {props.name}</h1>
   );
}

const element = (
   <Greeting name="Dennis" />
);

Let's understand what's going on here.

The props parameter of Greeting is an object containing all the props passed in while instantiating a <Greeting> element.

In our case, we just pass in a name prop to the <Greeting> element, and so this element invokes Greeting with its first argument object containing this name property.

We access the provided object as props (this is a conventional parameter name in React), and then the property name on it before finally rendering it inside the <h1>.

Simple.

See how using a prop, we've effectively made Greeting reusable. We can use this Greeting component as many times as we want to, with any name whatsoever.

Following we demonstrate creating two Greeting instances, nested inside a <div> element:

function Greeting(props) {
   return (
      <h1>Hello {props.name}</h1>
   );
}

const element = (
   <div>
<Greeting name="Dennis" /> <Greeting name="Ada" />
</div> );
Live Example

Using components, we can simplify our apps by creating the minimal number of these reusable pieces and then using them wherever the need be.

Purely remarkable approach of React.

Now when accessing props in a component, there is a widely used convention, following from modern-day JavaScript. That is to destructure the object into local variables named after the given props.

Applying this idea to our Greeting component, here's how we'd define it:

function Greeting({name}) {
   return (
      <h1>Hello {name}</h1>
   );
}
Live Example

The {name} expression inside the pair of parentheses (()) extracts the property name out of the given argument object and creates a local variable name holding the same value.

If we have two props, we would have two destructured properties and, thus, two variables:

function Greeting({name, language}) {
   return (
      <div>
         <h1>Hello {name}.</h1>
         <p>You are programming in {language}.</p>
      </div>
   );
}

If we have three props, we'd have three variables; if four props, then four variables; and so on and so forth.

At one point, you might say that this won't be scalable in the longer run. What if we have 20 props? Would we create each one of these as a separate variable?

Well, in such a case, we could leverage the rest operator (...). The idea is to create variables for very important props while group the rest of props, the less important ones, into a separate object using the rest operator.

An example follows:

function Greeting({name, language, ...lessImportantProps}) {
   return (
      <div>
         <h1>Hello {name}.</h1>
         <p>You are programming in {language}.</p>
         <p {...lessImportantProps} />
      </div>
   );
}

Here, the name and language props are useful for the Greeting component, and hence used to create local variables while the rest of the props are meant only for the second <p> element and thus grouped into lessImportantProps and then this object passed to the <p> element.

Obviously, in a real app, we won't call this object as lessImportantProps — this is only used for the sake of explanation. The common convention is to just call it props as shown below:

function Greeting({name, language, ...props}) {
   return (
      <div>
         <h1>Hello {name}.</h1>
         <p>You are programming in {language}.</p>
         <p {...props} />
      </div>
   );
}

As you'll start creating more complex components, composed of other components, you'll realize that this approach is extremely useful.

In particular, the props that are used by a component are used to create local variables in it while the remaining props are passed down to the sub-components using the spread attribute (...{props}) in JSX.

To be more specific, this approach is mainstream in prop drilling. Wait, what?

Prop drilling

As we learnt before, components can be made out of components themselves. Now, if we want to pass on props to those sub-components, we obviously need to start off passing props from the top-level component.

Let's see what this means.

Suppose we have our Greeting component made up of a heading and a body. The heading is denoted using another GreetingHeading component while the body is denoted using a GreetingBody component.

Here's the definition of GreetingHeading:

function GreetingHeading({name}) {
   return (
      <h1>Hello, {name}.</h1>
   );
}

And here's the definition of GreetingBody:

function GreetingBody({language}) {
   return (
      <p>You are programming in {language}.</p>
   );
}

Now, let's see the composition of the Greeting component:

function Greeting({name, language}) {
   return (
      <div>
         <GreetingHeading name={name} />
         <GreetingBody language={language} />
      </div>
   );
}

It returns back a <div> holding the two elements, <GreetingHeading> and <GreetingBody>.

A common gotcha when returning multiple elements!

Notice the <div> in the code above. It's purely meant to group the <GreetingHeading> and <GreetingBody> elements. But why do we need to group them?

Well, let's find out the answer by removing the <div>.

function Greeting({name, language}) {
   return (
      <GreetingHeading name={name} />
      <GreetingBody language={language} />
   );
}

Try running this code. What do you get? Well, it ends up in a compilation error.

On the front, the code might seem absolutely fine — after all, we're returning HTML-like syntax — but if we recall how a JSX element gets converted to a createElement() expression, we'll be able to see the issue.

That is, the code above gets converted to the following:

function Greeting({name, language}) {
   return (
createElement(GreetingHeading, {name: name})
createElement(GreetingBody, {language: language}) ); }

And there you have it — we have two createElement() calls next to one another, and that's invalid JavaScript.

One solution that was mainstream in the past but has been replaced with other better solutions is to use a <div> as we did above.

Another solution is to use an array, as follows:

function Greeting({name, language}) {
   return ([
      createElement(GreetingHeading, {name: name})
      createElement(GreetingBody, {language: language})
   ]);
}

The reason why an array works here is simply based on the definition of a React node provided to createElement() or to the render() method from React DOM.

However, using an array literal in this way is not really a desired option; we're writing JSX elements and, likewise, should ideally be using element-like constructs instead of array literals.

The best solution here is to use a React fragment but we'll defer that to be explored in the React Fragments chapter.

Let's now instantiate a Greeting element:

const element = (
   <Greeting name="Dennis" language="C" />
);

And now let's see the output produced:

Live Example

As can be seen, the <h1> and <p> elements indeed get rendered with the correct text inside them. This is the potential of props.

We pass on the name and language props to Greeting which then passes them on to GreetingHeading and GreetingBody, each of which then use the prop passed to it for its own content.

For now, we had only one level of nesting inside Greeting. However, we could have any nesting depth as we want to.

And whatever the nesting be, if lower-level components require given props and if those props are passed on to the top-level components, then those props have to be passed down at each new level to ultimately reach the desired lowest-level components.

In fact, there's a special term to refer to this: prop drilling.

Prop drilling is the act of consecutively passing over props from higher-level components to lower-level components until those props reach the components they are consumed by.

While React has, no doubt, innovated quite a lot in the JavaScript application development space, it has done so in naming as well. 'Prop drilling,' quite cool, isn't it?

Prop drilling also means that we pass certain props to components that have absolutely nothing to do them — they merely take the props for the sake of passing them over to lower-level components.

This might not be a big issue for even some complex apps, but there would eventually come a point where it does start to become one. In that case, we could use contexts in React. We'll learn more about contexts later on in this course.

Using the children prop

Suppose we have a Button component that takes in the text to be put inside a <button> element and then returns back that element.

You might ask the purpose of using Button instead of just a normal HTML <button>.

Well, Button is a really handy way to denote buttons in an app. The entire logic of a button sits nicely inside the component. If we want to, let's say, change the class of all buttons to 'app-btn', we just need to go to the Button component and make this change.

Abstracting away the creation of <button> elements directly with the help of a Button component is highly convenient in a large app.

Now we could define Button as follows, with a text prop:

function Button({text}) {
   return (
      <button className="btn">{text}</button>
   );
}

Using this Button component, we can easily create a button in our app with given text:

/* ... */

const element = (
   <Button text="This is a custom button." />
);

Live Example

But notice that currently we're only capable of providing the text to be used inside the <button> via the text prop of Button. This isn't how we're used to working with buttons in HTML.

In HTML, we are used to doing the following to add content inside a <button>:

<button>Some <b>important</b> text.</button>

The content of the button is provided directly between the <button> and </button> tags.

Fortunately, we could emulate this same syntax using React components as well, all thanks to the children prop.

In the React Elements chapter, we learnt that the children prop holds all of the children passed to a given element at the time of its creation, in the form of an array.

For example, the following JSX:

<div>
   <h1>Heading</h1>
   <p>A paragraph</p>
</div>

gets converted to the following createElement() expression:

createElement('div', null,
createElement('h1', null, 'Heading'),
createElement('p', null, 'A paragraph') )

Notice the arguments to createElement starting from the third one (as highlighted). Internally, React combines these into one array, and then provides the array to the element in the form of the children prop.

In simpler words, the children prop allows a component to access the content given between its opening and closing tags.

So how could we use this to emulate the <button> syntax common to us in HTML?

The idea is to retrieve the value of the children prop inside Button and then dump it inside <button>.

Let's redefine our <Button> component to work this way:

function Button({children}) {
   return (
      <button className="btn">{children}</button>
   );
}

Here's what's happening here:

The children prop is extracted from the given set of props and is then rendered inside the <button> element. This simply means that whatever is provided inside a <Button> element effectively become the children of the returned <button> element.

Now, let's try creating two <Button>s in our app:

/* ... */

const element = (
   <div>
      <Button>Button with normal text</Button>
      <br/><br/>
      <Button>Button with <em>emphasized</em> text</Button>
   </div>
);

The first button has just some plain text in it while the second one has text along with an <em> element in it.

The <br/> tag here obviously denotes the <br> HTML element, and is only used to be able to separate apart both the buttons as they're painted on the screen.

Let's see the output:

Live Example

As you can see in the link above, each renderd button indeed has the content shown above.

This is nothing but amazing!

The defacto App component

Let's now talk about structuring our React applications in a way that sits with the convention.

The file where the bundling begins is typically index.js. This index.js file usually imports an App component from another file called App.jsx (in the same src directory).

Notice the .jsx file extension in App.jsx; it denotes a JSX file. We learnt about JSX files in the React JSX: JSX files.

index.js looks something along the following lines:

index.js
import React from 'react';
import ReactDOM from 'react-dom';

import App from './App';

ReactDOM.createRoot(document.querySelector('#root')).render(
   <App/>
);

Here's the App.jsx file:

App.jsx
import React from 'react';

// The entry into our app.
function App() {
   return (
      <h1>Hello World!</h1>
   );
}

export default App;

App.jsx defines an App component that is the entry into our app. More precisely, the App component defines the top-most, root element of the tree of React elements living inside #root.

It's important not to mix up the purpose of index.js with that of App.jsx.

  • The file index.js serves to call on to other libraries, such as React DOM, and initialize our app. It's the file where the bundling process begins.
  • In contrast, App.jsx defines the application itself.

From this point onwards, we'll be following this convention, unless stated otherwise.

That is, we'll only show code for the App component, assuming that it is appropriately imported by the index.js file (which itself contains all the configurations and initializations).

Separate components, separate files

As our apps become complex and more involved, our components grow in size and number as well. This clearly means that we can't have all the components in one single file.

In React, it's yet another convention to have components in separate files. This holds even for rudimentary apps.

One component maps to one file, named after the component.

So, for instance, if we have a Greeting component, we'd put it inside the Greeting.jsx file, and customarily make it the default export of the file.

Shown below is a concrete example.

Following, we have our Greeting component defined inside its own Greeting.jsx file:

Greeting.jsx
import React from 'react';

function Greeting() {
   return (
      <h1>Hello World!</h1>
   );
}

export default Greeting;

This component gets imported in App.jsx and rendered inside App:

App.jsx
import React from 'react';

function App() {
   return (
      <Greeting/>
   );
}

export default App;

Usually, this approach of having one and only one component per file works without any issues. However, it doesn't always have to be the case.

In particular, when a component requires other components that make sense to be used with it only, we might have those other components in the same file.

For example, a Table component might have a related component called TableRow. This component might just be placed in the same file as Table because it will only ever be used by Table.

The underlying file in this case would most probably be called Table.jsx since the Table component is the main component in the file.

Moving on

Without any doubts whatsoever, components sure are a cornerstone of React.

In this chapter, we got to learn a great deal of information regarding components in React, such as what are components, how to define components, components vs. component instances, some common conventions surrounding the usage of components, and so on.

However, this is just the beginning of what's possible with React. In the next chapters, we'll be covering other highly fruitful information to be able to create complex programs. And in these programs, we'll be heavily using ideas that we learnt in this chapter.

So before you move forward to the next React Fragments chapter, make sure to be absolutely confident with your knowledge of React components, for they are a fundamental building block of all kinds of React applications.