DEV Community

Cover image for Ever wonder what React does?
Yohann Legrand
Yohann Legrand

Posted on

Ever wonder what React does?

Photo by Ferenc Almasi @unsplash

Read this article on my blog

When we use React and JSX in our web applications, it's important to remember that we are "just" using an abstraction of the browser API methods.

In fact, JavaScript has a set of imperative methods that you can use to interact with the DOM, while React abstracts those methods to offer your a declarative approach.

πŸ’‘ If you're not sure what "imperative" and "declarative" means, here's a brief explanation:

  • Imperative is a concept that implies telling HOW to do something (technically speaking)
  • Declarative implies telling WHAT to do

This is why it's called an abstraction because we don't need to know HOW it's gonna be done, we just want it done. For more details about those concepts, I recommend you check this great article.

I think it's important (and interesting) to understand how these abstractions work, what they do, and how they do it. This gives you more confidence as a developer and allows you to use them more efficiently.

So, let me take you on a quick journey from the good old times to the beautiful React components of nowadays πŸš€

1. The imperative way

Let's see how you can interact with the browser DOM with pure JavaScript. Our goal here is to render a paragraph on the page.

<!-- index.html -->
<body>
  <script type="text/javascript">
    // First, we need to create a div that will be the root element
    const rootNode = document.createElement("div")
    // Let's give it the id "root" and the class "container"
    rootNode.setAttribute("id", "root")
    rootNode.setAttribute("class", "container")
    // And finally add it to the DOM
    document.body.append(rootNode)

    // Sweet πŸ‘Œ Now we need to create our paragraph
    const paragraph = document.createElement("p")
    paragraph.textContent = "Welcome, dear user !"
    // and add it to the root div
    rootNode.append(paragraph)
  </script>
</body>
Enter fullscreen mode Exit fullscreen mode

So basically, we tell the browser to create a div with the id root and the class container, and to insert it inside the body tag. Then we create and add a paragraph inside that div. Here's the output:

the imperative way - output

2. React APIs

Now let's change this to use React. We actually only need 2 packages:

  1. React: responsible for creating React elements
  2. ReactDOM: responsible for rendering those elements to the DOM

React supports multiple platforms. ReactDOM is used when writing web applications. For mobile apps, you would use the appropriate React Native package to render your elements.

Note: normally, you would create your project with a tool like create-react-app or get React and ReactDOM scripts from a package registry. Here we use a CDN for demonstration purposes

<!-- index.html -->

<body>
  <!-- The root div is placed directly in the HTML -->
  <!-- We could also create it like before, and append it to the body -->
  <div id="root"></div>

  <!-- We import React and ReactDOM -->
  <script src="https://unpkg.com/react@17.0.0/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@17.0.0/umd/react-dom.development.js"></script>

  <script type="module">
    const rootNode = document.getElementById("root")

    // Create the paragraph
    const element = React.createElement("p", null, "Welcome, dear user !")

    // Render the paragraph inside the root node
    ReactDOM.render(element, rootNode)
  </script>
</body>
Enter fullscreen mode Exit fullscreen mode

With this in place, the generated HTML is exactly the same as before, with the extra imports for React and ReactDOM:

React APIs - output

React.createElement() takes three arguments: type, props and children. This means that if we wanted our paragraph to have the className "welcome-text", we would pass it as a prop:

React.createElement("p", { className: "welcome-text" }, "Welcome, dear user !")
Enter fullscreen mode Exit fullscreen mode

We could also pass the children as prop, instead of passing it as the third argument:

React.createElement("p", {
  className: "welcome-text",
  children: "Welcome, dear user !",
})
Enter fullscreen mode Exit fullscreen mode

The children prop can take an array for multiple children, so we could also do:

React.createElement("p", {
  className: "welcome-text",
  children: ["Welcome,", "dear user !"],
})
Enter fullscreen mode Exit fullscreen mode

Or we can even add all the children after the second argument, as individual arguments:

React.createElement(
  "p",
  { className: "welcome-text" },
  "Welcome, ",
  "dear user !"
)
Enter fullscreen mode Exit fullscreen mode

If you're curious about the element returned by React.createElement, it's actually quite a simple object that looks like this:

{
  type: "p",
  key: null,
  ref: null,
  props: { className: "welcome-text", children: ["Welcome, ", "dear user !"]},
  _owner: null,
  _store: {}
}
Enter fullscreen mode Exit fullscreen mode

The renderer's job, in our case ReactDOM.render's job, is simply to interpret that object and create the DOM nodes for the browser to print. This is why React has a different renderer for each supported platform: the output will vary depending on the platform.

So, this is all great, but you can start to see what a pain it would be the create more complex UI by using just those APIs. For example, let's say we need to make the following changes to our page:

  • Place the paragraph inside a div
  • Give the div an id "container"
  • "dear user" should be in bold
  • Place a button inside the div, with the text "Say Hi" that logs "Hi !" in the console when clicked

Here's how we would implement those changes:

<!-- index.html -->
<body>
  <div id="root"></div>

  <script src="https://unpkg.com/react@17.0.0/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@17.0.0/umd/react-dom.development.js"></script>

  <script type="module">
    const rootNode = document.getElementById("root")

    // Container div
    const element = React.createElement("div", {
      id: "container",
      children: [
        // Paragraph
        React.createElement("p", {
          className: "welcome-text",
          children: [
            "Welcome, ",
            // "dear user" text wrapped inside a strong tag
            React.createElement("strong", null, "dear user"),
            " !",
          ],
        }),
        // "Say Hi" button
        React.createElement("button", {
          onClick: () => console.log("Hi !"),
          children: "Say Hi",
        }),
      ],
    })

    // Render the paragraph inside the root node
    ReactDOM.render(element, rootNode)
  </script>
</body>
Enter fullscreen mode Exit fullscreen mode

HTML Output:

<div id="root">
  <div id="container">
    <p class="welcome-text">Welcome, <strong>dear user</strong> !</p>
    <button>Say Hi</button>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

While it works perfectly, I think it is safe to say that nobody wants to build UIs like this. And this is where JSX comes in.

3. JSX to the rescue

JSX is a syntax extension to JavaScript, and it allows us to do things like this:

const paragraph = <p className="welcome-text">Welcome, dear user !</p>
Enter fullscreen mode Exit fullscreen mode

The browser won't understand this by itself, so we need a compiler like Babel which will turn this code into a React.createElement call:

const paragraph = React.createElement(
  "p",
  {
    className: "welcome-text",
  },
  "Welcome, dear user !"
)
Enter fullscreen mode Exit fullscreen mode

JSX's power, besides being able to nest elements in an HTML-like way, resides in what is called "interpolation". Everything you put inside { and } will be left alone and use to compute the values of props and children of createElement :

const ui = (
  <div id="greetings">
    Hello {firstname} {lastname} !
  </div>
)
Enter fullscreen mode Exit fullscreen mode

Compiled version:

const ui = React.createElement(
  "div",
  {
    id: "greetings",
  },
  "Hello ",
  firstname,
  " ",
  lastname,
  " !"
)
Enter fullscreen mode Exit fullscreen mode

With JSX in our toolbox, we can now rewrite the previous implementation in a much more clean and easy way. We will include Babel as a CDN and change our script type to text/babel so that our JSX expressions get compiled down to React.createElement calls:

<!-- index.html -->
<body>
  <div id="root"></div>

  <script src="https://unpkg.com/react@17.0.0/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@17.0.0/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/babel-standalone@6.26.0/babel.js"></script>

  <script type="text/babel">
    const rootNode = document.getElementById("root")

    // Container div
    const element = (
      <div id="container">
        <p className="welcome-text">
          Welcome, <strong>dear user</strong> !
        </p>
        <button onClick={() => console.log("Hi !")}>Say Hi</button>
      </div>
    )

    // Render the paragraph inside the root node
    ReactDOM.render(element, rootNode)
  </script>
</body>
Enter fullscreen mode Exit fullscreen mode

Much better πŸ˜‡ Back in the browser, we can see our UI with the generated DOM (including our untouched "text/babel" script):

jsx output

If we take a look in the <head> tag, we can see that Babel added a script for us with the compiled version of our JavaScript and JSX:

jsx compiled output

Babel basically compiles down all our JSX code to nested React.createElement calls for us. How nice of him. Thanks to interpolation, we can also use variables for things that we want to use more than once in our JSX:

const rootNode = document.getElementById("root")

const greetingButton = (
  <button onClick={() => console.log("Hi !")}>Say Hi</button>
)

// Container div
const element = (
  <div id="container">
    {greetingButton}
    <p className="welcome-text">
      Welcome, <strong>dear user</strong> !
    </p>
    {greetingButton}
  </div>
)

// Render the paragraph inside the root node
ReactDOM.render(element, rootNode)
Enter fullscreen mode Exit fullscreen mode

jsx interpolation

Compiled version (thanks again, Babel !):

var rootNode = document.getElementById("root")

var greetingButton = React.createElement(
  "button",
  {
    onClick: function onClick() {
      return console.log("Hi !")
    },
  },
  "Say Hi"
)

// Container div
var element = React.createElement(
  "div",
  { id: "container" },
  greetingButton,
  React.createElement(
    "p",
    { className: "welcome-text" },
    "Welcome, ",
    React.createElement("strong", null, "dear user"),
    " !"
  ),
  greetingButton
)

// Render the paragraph inside the root node
ReactDOM.render(element, rootNode)
Enter fullscreen mode Exit fullscreen mode

Now we could use a function instead of a variable for our button. This way, we could pass as props the text for the button and the message to log in the console:

const rootNode = document.getElementById("root")

const greetingButton = (props) => (
  <button onClick={() => console.log(props.message)}>{props.children}</button>
)

// Container div
const element = (
  <div id="container">
    {greetingButton({ message: "Hi !", children: "Say Hi" })}
    <p className="welcome-text">
      Welcome, <strong>dear user</strong> !
    </p>
    {greetingButton({ message: "Bye !", children: "Say Bye" })}
  </div>
)

// Render the paragraph inside the root node
ReactDOM.render(element, rootNode)
Enter fullscreen mode Exit fullscreen mode

And if we look at the compiled version of our greetingButton function:

var rootNode = document.getElementById("root")

var greetingButton = function greetingButton(props) {
  return React.createElement(
    "button",
    {
      onClick: function onClick() {
        return console.log(props.message)
      },
    },
    props.children
  )
}

// Container div
var element = React.createElement(
  "div",
  { id: "container" },
  greetingButton({ message: "Hi !", children: "Say Hi" }),
  React.createElement(
    "p",
    { className: "welcome-text" },
    "Welcome, ",
    React.createElement("strong", null, "dear user"),
    " !"
  ),
  greetingButton({ message: "Bye !", children: "Say Bye" })
)

// Render the paragraph inside the root node
ReactDOM.render(element, rootNode)
Enter fullscreen mode Exit fullscreen mode

We see that it is now a function returning a React.createElement, and its value is used as a children argument of the createElement for the main element.

I think you see where this is going...

4. React Components

With our greetingButton, we are one step away from the traditional React Components. In fact, it would be nice to be able to use it like this:

const element = (
  <div id="container">
    <greetingButton message="Hi !">Say Hi</greetingButton>
    <p className="welcome-text">
      Welcome, <strong>dear user</strong> !
    </p>
    <greetingButton message="Bye !">Say Bye</greetingButton>
  </div>
)
Enter fullscreen mode Exit fullscreen mode

But here's what happens if we do so, back in the browser:

wrong casing for react component

The buttons aren't "buttons", we just see their texts (= children) in the page. Because <greetingButton> is in the DOM without being a valid HTML tag, the browser doesn't know what to do with it. ReactDOM is telling us why in the console:

Warning: <greetingButton /> is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.

Warning: The tag <greetingButton> is unrecognized in this browser. If you meant to render a React component, start its name with an uppercase letter.

Because greetingButton doesn't start with an uppercase letter, Babel compiles our code to this:

React.createElement("greetingButton", { message: "Hi !" }, "Say Hi"),
// ...
React.createElement("greetingButton", { message: "Bye !" }, "Say Bye")
Enter fullscreen mode Exit fullscreen mode

greetingButton is used as string for the type of the element, which results in a greetingButton HTML tag that the browser don't understand.

So let's change our greetingButton to be a React Component:

const rootNode = document.getElementById("root")

const GreetingButton = (props) => (
  <button onClick={() => console.log(props.message)}>{props.children}</button>
)

// Container div
const element = (
  <div id="container">
    <GreetingButton message="Hi !">Say Hi</GreetingButton>
    <p className="welcome-text">
      Welcome, <strong>dear user</strong> !
    </p>
    {/** This is functionnaly equivalent to the other GreetingButton */}
    <GreetingButton message="Bye !" children="Say Bye" />
  </div>
)

// Render the paragraph inside the root node
ReactDOM.render(element, rootNode)
Enter fullscreen mode Exit fullscreen mode

Starting to look familiar ? πŸ˜‰ Let's take a look at the compiled code:

var rootNode = document.getElementById("root")

var GreetingButton = function GreetingButton(props) {
  return React.createElement(
    "button",
    {
      onClick: function onClick() {
        return console.log(props.message)
      },
    },
    props.children
  )
}

// Container div
var element = React.createElement(
  "div",
  { id: "container" },
  React.createElement(GreetingButton, { message: "Hi !" }, "Say Hi"),
  React.createElement(
    "p",
    { className: "welcome-text" },
    "Welcome, ",
    React.createElement("strong", null, "dear user"),
    " !"
  ),
  React.createElement(GreetingButton, { message: "Bye !" }, "Say Bye")
)

// Render the paragraph inside the root node
ReactDOM.render(element, rootNode)
Enter fullscreen mode Exit fullscreen mode

We can see that our component is now used as the type for React.createElement, which is much better. At render time, our component (= function) will be called and the returned JSX will be injected in the DOM:

<div id="root">
  <div id="container">
    <button>Say Hi</button>
    <p class="welcome-text">Welcome, <strong>dear user</strong> !</p>
    <button>Say Bye</button>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

So, however you write your React Component, at the end of the day, it's just a function that returns JSX and it all gets compiled down to React.createElement:

const GreetingButton = (props) => (
  <button onClick={() => console.log(props.message)}>{props.children}</button>
)

// Same result, different writing:
function GreetingButton({ message, children }) {
  return <button onClick={() => console.log(message)}>{children}</button>
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

I hope you learned a few things by reading this post. I think this is really interesting to know what's going on "under the hood" when writing React Components. The more you can compile down JSX in your head, the more efficient you will be using it. Feel free to play around in the Babel playground to see what's the output of the JSX you write in real-time!

This post was inspired by this great article by Kent C. Dodds: What is JSX?

Top comments (0)