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>
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:
2. React APIs
Now let's change this to use React. We actually only need 2 packages:
- React: responsible for creating React elements
- 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>
With this in place, the generated HTML is exactly the same as before, with the extra imports for React and ReactDOM:
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 !")
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 !",
})
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 !"],
})
Or we can even add all the children after the second argument, as individual arguments:
React.createElement(
"p",
{ className: "welcome-text" },
"Welcome, ",
"dear user !"
)
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: {}
}
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>
HTML Output:
<div id="root">
<div id="container">
<p class="welcome-text">Welcome, <strong>dear user</strong> !</p>
<button>Say Hi</button>
</div>
</div>
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>
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 !"
)
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>
)
Compiled version:
const ui = React.createElement(
"div",
{
id: "greetings",
},
"Hello ",
firstname,
" ",
lastname,
" !"
)
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>
Much better ๐ Back in the browser, we can see our UI with the generated DOM (including our untouched "text/babel" script):
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:
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)
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)
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)
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)
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>
)
But here's what happens if we do so, back in the browser:
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")
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)
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)
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>
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>
}
Conclusion
I hope you learnt 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?