What is JSX and How Does it Work?
JSX is an XML-like syntax extension to JavaScript. It is similar to a template language, but it has the full power of JavaScript. JSX gets compiled to React.createElement() calls which return plain JavaScript objects called “React elements”. To get a basic introduction to JSX see the docs here.
In this article, we will take a look at how JSX works and how to create custom components using JSX without using any frameworks.
Let's get started
Create a new Project
Create a directory for your project by running the following command in your terminal:
mkdir jsx-demo
cd jsx-demo
npm init -y
Install Babel
Babel is a JavaScript compiler. It converts ECMAScript code into a backward-compatible version of JavaScript in current and older browsers or environments, Babel is used to compiling JSX to JavaScript.
npm install --save-dev @babel/core @babel/cli @babel/plugin-transform-react-jsx
Create a babel.config.js
file
const presets = [];
const plugins = [
[
"@babel/plugin-transform-react-jsx",
{ runtime: "automatic", importSource: "./core" },
],
];
module.exports = {
presets,
plugins,
};
NOTE: The importSource
option is used to specify the path to the module that will be used to create the JSX elements. In this case, we are using the core
module. We will create this module in the next step.
Create a src directory in your project, This is where we will write our code.
mkdir src
cd src
touch core/jsx-runtime.js
Let's create an index.html
file in the root directory of our project
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>My App</title>
</head>
<body>
<h1>Hello Friends</h1>
<div id="root"></div>
</body>
</html>
NOTE: The id
of the root element is important. It is used to mount the application. We will see how to do this in the next step.
Let's create App.jsx and Button.jsx
We will create a App.jsx
and Button.jsx
files in the src
directory. App.jsx will be the entry point for our project.
Button.jsx
const Button = ({ children, onClick }) => (
<button onClick={onClick}>{children}</button>
);
export default Button;
App.jsx
import Button from "./Button";
const App = () => (
<div>
<Button onClick={() => alert(1)}>Click 11</Button>
<Button onClick={() => alert(2)}>Click 12</Button>
<Button onClick={() => alert(3)}>Click 13</Button>
</div>
);
const rootElement = document.getElementById("root");
rootElement.appendChild(<App />);
export default App;
Let's add the babel script in package.json
This script will compile our JSX code to JavaScript code. The output will be in the dist
directory.
"scripts": {
"build-babel": "babel src -d dist",
},
Let's compile our code
npm run build-babel
Let's see how it looks like.
import Button from "./Button";
import { jsx as _jsx } from "./core/jsx-runtime";
import { jsxs as _jsxs } from "./core/jsx-runtime";
const App = () =>
_jsxs("div", {
children: [
_jsx(Button, {
onClick: () => alert(1),
children: "Click 11",
}),
_jsx(Button, {
onClick: () => alert(2),
children: "Click 12",
}),
_jsx(Button, {
onClick: () => alert(3),
children: "Click 13",
}),
],
});
const rootElement = document.getElementById("root");
rootElement.appendChild(_jsx(App, {}));
export default App;
NOTE:
- The
jsx
function is imported from thecore
module. We added thebabel.config.js
file, but we have not createdjsx-runtime
yet. We will create it in the next step. - JSX code is converted to JavaScript function, Which will call the
jsx
function.
Most important part of this article is creating the jsx-runtime
module. This is where the magic happens.
All the components import the jsx
function from the jsx-runtime
module. The jsx
function is responsible for creating the elements.
const add = (parent, child) => {
parent.appendChild(child?.nodeType ? child : document.createTextNode(child));
};
const appendChild = (parent, child) => {
if (Array.isArray(child)) {
child.forEach((nestedChild) => appendChild(parent, nestedChild));
} else {
add(parent, child);
}
};
export const jsx = (tag, props) => {
const { children } = props;
if (typeof tag === "function") return tag(props, children);
const element = document.createElement(tag);
Object.entries(props || {}).forEach(([name, value]) => {
if (name.startsWith("on") && name.toLowerCase() in window) {
element.addEventListener(name.toLowerCase().substr(2), value);
} else {
element.setAttribute(name, value);
}
});
appendChild(element, children);
return element;
};
export const jsxs = jsx;
Let's understand how the jsx
function works.
As you can see, the jsx
function takes two arguments, the first argument is the tag name and the second argument is the props. The jsx
function returns a plain JavaScript object. If the tag is a function, it will call the function and return the result. If the tag is a string, it will create a DOM element, set all the props to the element, and return the element.
The last step creates a bundle file using webpack And injects the bundle file into the index.html
file.
Install webpack, webpack-cli and html-webpack-plugin using the following command:
npm install --save-dev webpack webpack-cli html-webpack-plugin
NOTE: We can also configure babel in the webpack config, But I'm using babel separately to get a better understanding of how JSX code is compiled.
Create a webpack.config.js file
Add the following code to the webpack.config.js
file.
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: "./dist/App.js",
mode: "production",
output: {
path: `${__dirname}/build`,
filename: "bundle.js",
},
plugins: [
new HtmlWebpackPlugin({
template: "src/index.html",
}),
],
};
NOTE: The entry
is the path of the compiled file. The output
is the path where the bundle file will be created. The template
is the path of the index.html
file.
Update package.json scripts
"scripts": {
"start": "webpack serve --open",
"build-babel": "babel src -d dist",
"build-webpack": "webpack --config webpack.config.js",
"build": "npm run build-babel && npm run build-webpack"
},
NOTE: The start
script will start the webpack dev server. The build
script will compile the code using babel and create the bundle file using webpack.
Time to build our project
npm run build
After running the above command, you will see a build
directory in your project. This is where the compiled code will be saved. Also, the dist directory will have the compiled code. We can delete the dist directory if we want, I would recommend keeping it for reference.
Run the project
npm run start
Quick Tip: You can use serve package to start the HTTP server from any directory. This is very useful when you are working on a static website.
cd build
npx serve
Live Demo: Here
In the next article, We will also add a state to this app.
Thank you for reading 😊
Got any additional questions? please leave a comment.
Must Read If you haven't
3 steps to create custom state management library with React Hooks and Context API
Rahul Sharma ・ Mar 15 '22
How to cancel Javascript API request with AbortController
Rahul Sharma ・ Apr 9 '22
Getting started with SolidJs – A Beginner's Guide
Rahul Sharma ・ May 20 '22
More content at Dev.to.
Catch me on
Youtube Github LinkedIn Medium Stackblitz Hashnode HackerNoon
Top comments (4)
Hey, great article! Thank you. Can I find somewhere an article with adding state to this app?
@melone14 you can refer to this is not the state implementation but something similar.
stackblitz.com/edit/reactive-frame...
Hey, great article. Exactly what I was looking for. Thank you.
Here is a Typescript version of your jsx function:
const add = (parent: HTMLElement,
child: HTMLElement
| string
| number
| boolean
| null
| undefined) => {
parent.appendChild(child?.nodeType ? child : document.createTextNode(String(child)));
};
const appendChild = (parent: HTMLElement,
child: HTMLElement
| string
| number
| boolean
| null
| undefined) => {
if (Array.isArray(child)) {
child.forEach((nestedChild) => appendChild(parent, nestedChild));
} else {
add(parent, child);
}
};
export const jsx = (tag: string | Function,
props: { [key: string]: any }) => {
const { children, ...rest } = props;
if (typeof tag === "function") return tag({ ...rest, children });
const element = document.createElement(tag);
Object.entries(rest || {}).forEach(([name, value]) => {
if (name.startsWith("on") && name.toLowerCase() in window) {
element.addEventListener(name.toLowerCase().substr(2), value);
} else {
element.setAttribute(name, String(value));
}
});
appendChild(element, children);
return element;
};
export const jsxs = jsx;