JSX is the syntax recommended by React for describing the UI. After being processed by tools like Babel, JSX is transformed into plain JavaScript code. As the React document states:

Fundamentally, JSX just provides syntactic sugar for the React.createElement(component, props, … children) function.

That said, JSX is not necessary to write React code; any JSX code can be rewritten into plain JavaScript code. But the code using React. CreateElement can be very verbose and unreadable. JSX can greatly simplify code and provide development efficiency. Note that due to the birth of JSX, many people would assume that JSX was bound to React. In fact, JSX is a generic Template DSL. We could have used JSX in a Vue project, but instead of the react. createElement function call, we would have converted to H. Even libraries that do not use Virtual DOM technology can use JSX to describe the UI.

With that in mind, let’s go back to the title of the article. For libraries that use Virtual DOM technology, enhancing JSX is essentially enhancing the functions that create VDOM nodes. Since the function that creates the Virtual DOM node is usually named H, h will be used instead of react.createElement. Here is a brief example of how enhancing JSX might be done.

As mentioned above, Vue can also use JSX, but due to the flexibility of JSX in Vue, some features supported by JSX in Vue are not supported in React. For example, Vue’s JSX supports Class Prop. Classes support more forms, such as arrays, objects, and so on, than React’s className, which only accepts strings. Of course, the React community has libraries that do similar things, such as classnames and CLSX, to relieve developers of having to concatenate strings of classnames. However, this also means that the written tag will look like

. Obviously, this code looks a little bit messy compared to the implementation in Vue, and it also leads to the need to introduce the CLSX library in many files. As mentioned earlier, H determines what features JSX supports, so if we want to add functionality to JSX, we only need to enhance H. However, the enhancement of H is relatively low level, whether it is worth doing so still needs to be carefully measured.

Then, we will try to implement the enhancement of H. Before we start, let’s write a few tests.

import React from "react";
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";

test("class accepts an array".() = > {
  render(<div class={["a","b"]} >Hello World</div>);

  expect(screen.queryByText("Hello World")? .className).toBe("a b");
});

test("class accepts an object".() = > {
  render(<div class={{ a: false.b: true}} >Hello World</div>);

  expect(screen.queryByText("Hello World")? .className).toBe("b");
});

test("class appends to className".() = > {
  render(
    <div class={["b","c"]} className="a">
      Hello World
    </div>
  );

  expect(screen.queryByText("Hello World")? .className).toBe("a b c");
});
Copy the code

If this test is written in TypeScript, you’ll see that the TypeScript compiler will report class Prop errors. Property ‘class’ does not exist on type ‘DetailedHTMLProps

, HTMLDivElement>’ So we need to make some final changes to the TS type definitions to solve this problem.

Before we can enhance createElement, we need to know what function createElement is. Its first parameter, type, accepts various types of values. When creating intrinsic Elements, type is a string, such as div or SPAN. When you create an Element for a component, type is the component’s definition, either a class or a function. The second parameter is the props to the component, which is our focus. In this simple example, we just need to do some processing on the className value contained in the props to achieve the desired effect. The remaining parameters form the children of the component.

The general framework of the code is as follows:

import * as React from "react";
import clsx from "clsx";

const createElement = (type, props, ... children) = > {
  if (props == null| |! props.hasOwnProperty("class")) {
    returnReact.createElement(type, props, ... children); }// ...
};
Copy the code

If the props passed doesn’t contain className, we don’t need to do anything, just call React. CreateElement to create the element. If there is a className, the value is passed to CLSX for processing. Then we create the new props using the className returned.

const className = clsx(props["className"], props["class"]);

const newProps = { className };

for (let key in props) {
  if( hasOwnProperty.call(props, key) && key ! = ="class"&& key ! = ="className") { newProps[key] = props[key]; }}Copy the code

Finally, create element with the new props:

React.createElement(type, newProps, ... children);Copy the code

After that, we’ll add the TS type definition to this function. Create a.d.ts file with the following contents:

import * as React from "react";

declare const createElement: typeof React.createElement;

export default createElement;
Copy the code

We can then use this function to create element. The following test cases have been passed.

test("class accepts an array".() = > {
  render(createElement("div", { class: ["a"."b"]},"Hello World"));

  expect(screen.queryByText(/Hello World/)? .className).toBe("a b");
});
Copy the code

But at this point, we still can’t use Class Prop in JSX. To do this, we need to look more specifically at how Babel transforms JSX into ordinary JS statements. @babel/plugin-transform-react-jsx is a plugin included in @babel/preset- JSX conversion. The React Runtime plugin has two options. One of them is the React Classis Runtime. In this case,

<div className="red">Hello World</div>
Copy the code

Will be converted to

"use strict";

/*#__PURE__*/
React.createElement(
  "div",
  {
    className: "red",},"Hello World"
);
Copy the code

This is why we need to be able to access the React variable in JSX files. By setting pragma Option, we can:

Replace the function Used when compiling JSX Expressions.

With React Automatic Runtime, this import process is done automatically. Observe how the generated code differs under this Runtime:

"use strict";

var _jsxRuntime = require("react/jsx-runtime");

/*#__PURE__*/
(0, _jsxRuntime.jsx)("div", {
  className: "red".children: "Hello World"});Copy the code

As you can see, the creation of the Element in the generated code is left to the JSX function. Also, the code already includes code to introduce JSX. This means that the React variable no longer needs to be in scope. We can also see some differences between JSX’s function signature and that of react. createElement. By setting the importSource Option, we can:

Replaces the import source when importing functions.

An example configuration:

{
  "presets": [["@babel/preset-react",
      {
        "runtime": "automatic"."importSource": "preact"}}]]Copy the code

Both JSX Transforms are described in more detail in the React documentation.

To be compatible with both transforms, we need to add two files named jsx-Runtime.js and jsx-dev-runtime.js.

// jsx-runtime.js
import { mergeClassProp, hasOwnProperty } from "./utils";
import * as ReactJSXRuntime from "react/jsx-runtime";

export function jsx(type, props, key) {
  if(! hasOwnProperty.call(props,"class")) {
    return ReactJSXRuntime.jsx(type, props, key);
  }

  const newProps = mergeClassProp(props);

  return ReactJSXRuntime.jsx(type, newProps, key);
}

export function jsxs(type, props, key) {
  if(! hasOwnProperty.call(props,"class")) {
    return ReactJSXRuntime.jsxs(type, props, key);
  }

  const newProps = mergeClassProp(props);

  return ReactJSXRuntime.jsxs(type, newProps, key);
}
Copy the code
// jsx-dev-runtime.js
import { mergeClassProp, hasOwnProperty } from "./utils";
import * as ReactJSXENVRuntime from "react/jsx-dev-runtime";

export const Fragment = ReactJSXENVRuntime.Fragment;

export function jsxDEV(type, props, key, isStaicChildren, source, self) {
  if(! hasOwnProperty.call(props,"class")) {
    return ReactJSXENVRuntime.jsxDEV(
      type,
      props,
      key,
      isStaicChildren,
      source,
      self
    );
  }

  const newProps = mergeClassProp(props);

  return ReactJSXENVRuntime.jsxDEV(type, newProps, key);
}
Copy the code

The code is not that different from createElement, just doing a simple manipulation of the props passed in and creating the Element with the new props. In addition, we need to provide our own JSX type definitions:

// See @emotion/react
import "react";

import { ClassValue } from "clsx";

type WithConditionalClassProp<P> = "className" extends keyof P
  ? string extends P["className" & keyof P]
    ? { class? :ClassValue }
    : {}, {};export namespace CustomJSX {
  interface Element extends JSX.Element {}
  interface ElementClass extends JSX.ElementClass {}
  interface ElementAttributesProperty extends JSX.ElementAttributesProperty {}
  interface ElementChildrenAttribute extends JSX.ElementChildrenAttribute {}

  type LibraryManagedAttributes<C, P> = WithConditionalClassProp<P> &
    JSX.LibraryManagedAttributes<C, P>;

  interface IntrinsicAttributes extends JSX.IntrinsicAttributes {}

  type IntrinsicElements = {
    [K in keyof JSX.IntrinsicElements]: JSX.IntrinsicElements[K] & {
      class? :ClassValue;
    };
  };
}
Copy the code

Compared to the JSX definition provided by @Types/React, CustomJSX enhances the LibraryManagedAttributes and IntrinsicElements to enable components to accept class Prop.

After configuring Babel, we can use the JSX we provide to create Element. The complete code is now available on GitHub.