JSX is an extended JavaScript syntax popularized by React. The fundamental issue that JSX was designed to solve is that writing React code in vanilla JavaScript was simply too cumbersome:

// With JSX
return (
  <div>
    <h1>Contacts</h1>
    <ul>
      <li><a href="mailto:test1@test.com">test1@test.com</a></li>
      <li><a href="mailto:test2@test.com">test2@test.com</a></li>
    </ul>
  </div>
);

// Without JSX
return React.createElement('div', {}, 
  React.createElement('h1', {}, 'Contacts'),
  React.createElement('ul', {},
    React.createElement('li', {},
      React.createElement('a', {href: 'mailto:test1@test.com'}, 'test1@test.com')
    ),
    React.createElement('li', {},
      React.createElement('a', {href: 'mailto:test2@test.com'}, 'test2@test.com')
    )
  )
);

With the meteoric rise of the popularity of transpilers in 2015, largely due to delays in finalizing and adopting ES6, JSX became much more convenient. JSX let you essentially write HTML in your JavaScript, including syntax highlighting for the HTML portions Everyone was using transpilers anyway, what was one more syntactic extension?

But what does JSX actually do?

JSX Under the Hood

According to the JSX Spec Draft, JSX adds two new expressions to JavaScript syntax:

  • JSXElement
  • JSXFragment

A JSXElement is any JavaScript expression that starts with a <. This doesn't conflict with the < operator in JavaScript because starting an expression with < is a syntax error.

x < 5; // OK

< 5; // SyntaxError: Unexpected token <

A JSXElement is a recursive structure that the compiler is responsible for converting into function calls. Below is an example of nested JSXElement declarations:

<Node hello="world">
  <Node foo="bar"></Node>
</Node>

The JSX compiler converts the above syntax into the following JavaScript:

React.createElement(Node, { hello: 'world' },
  React.createElement(Node, { foo: 'bar' })
);

The JSX compiler converts the <Node prop="hello">... syntax into 3 function parameters:

  • elementType: the type of the node. In this case, Node.
  • props: a POJO containing the attributes. In this case, { hello: 'world' }
  • children: the rest of the parameters are the children of this element. In this case, it is the child Node element.

The JSX compiler parses all the element types, props, and children, and passes each of the nodes in the tree through a pragma. The pragma is just a fancy term for the function that JSX calls on each node. In the previous case, React.createElement was the pragma. But you can configure the pragma using an /** @jsx */ comment:

/** @jsx MyPragma */

<Node prop="hello">
  <Node prop="world"></Node>
</Node>

A JSX compiler would compile the above code into the below JavaScript:

/** @jsx MyPragma */
MyPragma(Node, { prop: 'hello' },
  MyPragma(Node, { prop: 'world' }));

A JSXFragment is simply a list of JSXElements without a parent. For the purposes of this article, I'll ignore fragments. You can read more about fragments here.

With different pragmas, JSX has some neat alternative use cases. Below are a few examples.

Building a Tree

Suppose you have a simple binary tree class:

function Node(val, left, right) {
  this.val = val;
  this.left = left;
  this.right = right;
}

Building a tree in vanilla JavaScript can get a little messy.

const tree = new Node(12,
  new Node(9),
  new Node(16,
    new Node(14)
  )
);

The above looks an awful lot like the unwieldy react.CreateElement() calls. You can use JSX to make defining a tree look much cleaner:

/** @jsx pragma */

const tree = <Node value="12">
  <Node value="9" />
  <Node value="14">
    <Node value="16" />
  </Node>
</Node>;

console.log(tree);

function pragma(Element, props, left, right) {
  return new Element(props.value, left, right);
}

function Node(val, left, right) {
  this.val = val;
  this.left = left;
  this.right = right;
}

Once compiled, the above JSX looks like this:

/** @jsx pragma */
var tree = pragma(Node, {
  value: "12"
}, pragma(Node, {
  value: "9"
}), pragma(Node, {
  value: "14"
}, pragma(Node, {
  value: "16"
})));
console.log(tree);

function pragma(Element, props, left, right) {
  return new Element(props.value, left, right);
}

function Node(val, left, right) {
  this.val = val;
  this.left = left;
  this.right = right;
}

And gives you the below output:

Node {
  val: '12',
  left: Node { val: '9', left: undefined, right: undefined },
  right:
   Node {
     val: '14',
     left: Node { val: '16', left: undefined, right: undefined },
     right: undefined } }

Express Route Definitions

Express is a popular HTTP server framework for Node.js. You can define an Express app by listing out method calls:

const express = require('express');

const app = express();
app.get('/status', getStatus);

// Nested sub-app
const apiSubapp = express.Router();
apiSubapp.get('/version', getVersion);
app.use('/api', apiSubapp);

You may use JSX to convert a XML-like route definition into Express .get() and .use() calls, as long as you use the right pragma. The key detail to note is that whatever the pragma() function returns for the child elements, that is what the parent node gets as children.

/** @jsx pragma */

const express = require('express');

const App = () => {};
const Get = () => {};
const Router = () => {};

const app = <App>
  <Get path="/status" handler={getStatus} />
  <Router path="/api">
    <Get path="/version" handler={getVersion} />
  </Router>
</App>;

function pragma(Element, props) {
  const children = Array.prototype.slice.call(arguments, 2);
  if (Element === App) {
    const app = express();
       for (const child of children) {
      app[child.method](child.path, child.handler);
    }
  } else if (Element === Get) {
    // Whatever you return here, the parent `App` or `Router` gets as
    // a `child`.
       return { handler: props.handler, path: props.path, method: 'get' }; 
  } else if (Element === Router) {
    const router = express.Router();
    // Let the children decide what `method`, `path`, and `handler`
    // to use.
    for (const child of children) {
      router[child.method](child.path, child.handler);
    }
    return { method: 'use', path: props.path, handler: router }; 
  }
}

function getStatus(req, res) {
  res.send('OK');
}

function getVersion(req, res) {
  res.json({ version: '1.0.0' });
}

Mongoose Schema Definitions

You can use JSX anywhere you have complex nested objects. So how about Mongoose schemas?

Below is a Mongoose schema with several nested properties and options:

const schema = new mongoose.Schema({
  email: {
    type: String,
    required: true
  },
  job: {
    company: String,
    title: String
  },
  address: new mongoose.Schema({
    street: {
      type: String,
      required: true
    },
    city: {
      type: String,
      required: true,
      trim: true
    },
    state: {
      type: String,
      required: true,
      enum: ['CA', 'NJ', 'NY', 'FL']
    }
  })
}, { id: false, bufferCommands: false });

With a little pragma magic, we can make the below JSX equivalent to the above schema definition:

const schema = <Schema id={false} bufferCommands={false}>
  <SchemaType name="email" type="String" required="true"></SchemaType>
  <Nested name="job">
    <SchemaType name="company" type="String"></SchemaType>
    <SchemaType name="title" type="String"></SchemaType>
  </Nested>
  <Schema name="address">
    <SchemaType name="street" type="string" required="true"></SchemaType>
    <SchemaType name="city" type="string" required="true" trim="true"></SchemaType>
    <SchemaType name="state" type="string" required="true" enum={['CA', 'NJ', 'NY', 'FL']}></SchemaType>
  </Schema>
</Schema>;

Below is the full code:

/** @jsx pragma */

const mongoose = require('mongoose');

const Schema = mongoose.Schema;
const SchemaType = mongoose.SchemaType;

// No-op since Mongoose doesn't have a "nested" SchemaType currently
function Nested(v) {
  return v;
}

const schema = <Schema id={false} bufferCommands={false}>
  <SchemaType name="email" type="String" required="true"></SchemaType>
  <Nested name="job">
    <SchemaType name="company" type="String"></SchemaType>
    <SchemaType name="title" type="String"></SchemaType>
  </Nested>
  <Schema name="address">
    <SchemaType name="street" type="string" required="true"></SchemaType>
    <SchemaType name="city" type="string" required="true" trim="true"></SchemaType>
    <SchemaType name="state" type="string" required="true" enum={['CA', 'NJ', 'NY', 'FL']}></SchemaType>
  </Schema>
</Schema>;

function pragma(Element, props) {
  const children = Array.prototype.slice.call(arguments, 2);
  if (Element === Schema) {
    if (props != null && props.name != null) {
      return { [props.name]: new Schema(children, props) };
    }
    return new Schema(children, props);
  } else if (Element === SchemaType) {
    return { [props.name]: props };
  } else if (Element === Nested) {
    const copy = { ...props };
    delete copy.name;
       return { [props.name]: copy };
  }
}
Found a typo or error? Open up a pull request! This post is available as markdown on Github
comments powered by Disqus