Ever wondered how does react jsx code (<div>Hello World</div>
) gets compiled to React.createElement("div", null, "Hello World")
. This blog is all about this compilation process by taking help from the source code of babel-preset-react and trying to build our own custom plugin.
Just to make things clear, I will not use Webpack at all because its significance lies in just bundling process. It has nothing to do with transpilation part. I will just use babel and 3 files. Thats it. No HTML nothing. The goal of this blog is to actually convert this jsx code to js code that browsers can really understand.
Github Link -> https://github.com/pulkitnagpal/custom-jsx-plugin
Before moving straight to the code, lets revise some basics
Basics
I tried this <div>Hello world</div>
code in a normal script tag and got "Unexpected token <". I thought create-react-app does everything under the hood and does some magic to convert it into React.createElement syntax.
Everyone must know that this div
in jsx is not an actual HTML Element. The compilation process just converts it into a function call. Not into HTML Element. That part is done by react.
I dug further and gradually realised that there is some power(apologies for using it :P) that converts this jsx into a function call like syntax. This power is harnessed by BABEL.
create-react-app and many other tools uses babel under the hood.
How does Babel works ?
- Parses your code => Babel converts your code to AST(Abstract Syntax Tree). Heavy term right ? No problem try this tool (https://astexplorer.net/). Try writing something on the left and a tree like structure will be generated on the right. This is done by a parser built inside babel.
- Traverse & Transform => This is where babel plugins and presets comes into play. A visitor pattern is provided by babel that let us traverse through all the tree nodes of AST and transform/manipulate those nodes into something we desire.
- Generate => This is the stage where babel converts the transformed tree back into human readable code.
Before moving to our own custom plugin, let's try to use already built react preset and transpile our index file using babel cli.
- Steps to install babel-cli are mentioned here
- Install React and ReactDOM and react preset
- Create an index.js file and .babelrc file
Add this to index file
ReactDOM.render(<div><p>Hello World</p></div>, document.getElementById("root"))
and this to .babelrc
{
"presets": ["react"]
}
Run this command on the terminal
node ./node_modules/babel-cli/bin/babel index.js
and we can see the transpiled code on the terminal screen. We can also create separate output file. But I wanted to make things simple. As we can see how this jsx code got transpiled to React createElement syntax. We will try to build our own plugin that will do the same thing.
NOTE: I will ignore the props and attributes part of the jsx in the custom plugin.
Custom-jsx-plugin
Clear the .babelrc file.
Create a new file custom-jsx-plugin.js
Try below code in (https://astexplorer.net/) to get an overview on how jsx code looks like in AST
function anything() {
return <div><p>Hello World</p></div>
}
and as we can see on the right side. The jsx part has a node type of JSXElement
. This is what we need to manipulate and replace it with a CallExpression
as React.createElement
is actually a javascript function.
When you try to parse this jsx using your local babel cli, you will get a syntax error. Because the parser does not know anything about the jsx syntax.
Thats why we need to add a file which manipulates the parser, name it as jsx-syntax-parser.js
jsx-syntax-parser.js
module.exports = function () {
return {
manipulateOptions: function manipulateOptions(opts, parserOpts) {
parserOpts.plugins.push("jsx");
}
};
};
and now our new .babelrc file will look like
{
"plugins": ["./custom-jsx-plugin", "./jsx-syntax-parser"]
}
The order of plugins matter and its actually in the reverse order. Right to Left. First our syntax parser will be executed which tells babel that it needs to parse jsx syntax also and then it will execute our custom plugin file which is for now empty.
As we still have not written anything inside our custom-jsx-plugin
file. The output of babel transpilation will be same as the index file. Nothing should have been changed.
Add this to custom-jsx-plugin
file
module.exports = function (babel) {
var t = babel.types;
return {
name: "custom-jsx-plugin",
visitor: {
JSXElement(path) {
//get the opening element from jsxElement node
var openingElement = path.node.openingElement;
//tagname is name of tag like div, p etc
var tagName = openingElement.name.name;
// arguments for React.createElement function
var args = [];
//adds "div" or any tag as a string as one of the argument
args.push(t.stringLiteral(tagName));
// as we are considering props as null for now
var attribs = t.nullLiteral();
//push props or other attributes which is null for now
args.push(attribs);
// order in AST Top to bottom -> (CallExpression => MemberExpression => Identifiers)
// below are the steps to create a callExpression
var reactIdentifier = t.identifier("React"); //object
var createElementIdentifier = t.identifier("createElement"); //property of object
var callee = t.memberExpression(reactIdentifier, createElementIdentifier)
var callExpression = t.callExpression(callee, args);
//now add children as a third argument
callExpression.arguments = callExpression.arguments.concat(path.node.children);
// replace jsxElement node with the call expression node made above
path.replaceWith(callExpression, path.node);
},
},
};
};
And thats it. These 12 lines of code can easily transpile our jsx code.
Run this command again on the terminal
node ./node_modules/babel-cli/bin/babel index.js
and notice that the result is the same as created by react-preset
like this
ReactDOM.render(React.createElement("div", null, React.createElement("p", null, Hello World)), document.getElementById("root"));
Explanation of the code
- In the visitor pattern of babel, during traversal of
the AST, for every
JSXElement
node, this callback function as defined above will be executed. - This node has two parts opening and closing elements. The name of opening element (eg "div") is extracted to be used as first argument of the function (React.createElement)
- The second argument (props or attributes) is considered as null for this example. Ignoring props just for simplicity.
- Now to create a function call, we will need to create 3
things CallExpression => MemberExpression => Identifiers.
The 2 identifiers used here are obviously
React
as an object andcreateElement
as the property. - Then we need to concat the rest arguments which are the child nodes of current node.
- At last we need to replace(used inbuilt function of path)
the current
JSXElement
node with thecallExpression
node you have created. This modifies the AST.
Conclusion
This is obviously not a production ready code. I have taken help from the source code of babel-preset-react and just to make things simpler I made the code shorter for better understanding. It is just the basic overview of how does this plugin works under the hood.
Top comments (0)