Custom elements in React using a custom JSX pragma
Tags: javascript, react, webcomponents, customelements
Published: 2019-07-08 19:05:16

You can test a sample project here:

{% github gugadev/react-ce-ubigeo %}

Online demo here:

{% codepen https://codepen.io/gugadev/pen/bPzrXp %}

> Hey, you can use web components in JSX code anyway.

Yeah, sure. However, there are certain use cases where you cannot use a web component following React guidelines, like passing complex properties such Objects and Arrays and binding _custom events_. So, what could we do as a workaround for these? Let's see.


## Passing objects/arrays to custom elements

There are some options. The easiest way is use `JSON.stringify` to pass it as a attribute:

```jsx
const App = () => {
  const data = [
    { x: 50, y: 25 },
    { x: 29, y: 47 }
  ]

  return (
    <h1>My awesome app</h1>
    <x-dataset data={JSON.stringify(data)} />
  )
}
```

Another option is use a ref to pass the object/array as property instead attribute:

```jsx
const App = () => {
  const ref = useRef()
  const data = [
    { x: 50, y: 25 },
    { x: 29, y: 47 }
  ]

  useEffect(() => {
    if (ref.current) {
      ref.current.data = data // set the property
    }
  })

  return (
    <h1>My awesome app</h1>
    <x-dataset ref={ref} />
  )
}
```

Hmm, I prefer the second one. And you?

## Binding custom events

This is a very common case when we deal with custom elements. When you need to attach a listener to a custom event, you need to use a ref and use `addEventListener` yourself.

```jsx
const App = () => {
  const ref = useRef()
  const data = [
    { x: 50, y: 25 },
    { x: 29, y: 47 }
  ]

  const customEventHandler = function(e) {
    const [realTarget] = e.composedPath()
    const extra = e.detail
    // do something with them
  }

  useEffect(() => {
    if (ref.current) {
      ref.current.data = data // set the property
      ref.current.addEventListener('custom-event', customEventHandler)
    }
  })

  return (
    <h1>My awesome app</h1>
    <x-dataset ref={ref} />
  )
}
```

Pretty simple, right? But, could we make it even easier? Yeah! using a custom JSX pragma. 

## Creating a custom JSX pragma

This is not a very simple way when we create the pragma, but, once that, you don't need to add aditional logic like example above. You will ended up using custom elements as any regular React component!

> The following code is a fork of [jsx-native-events](https://github.com/calebdwilliams/jsx-native-events) that I've extended adapt it to my needs.

First of all, what is a JSX pragma?

**JSX Pragma**

Pragma is just the function that transform JSX syntax to JavaScript. The default pragma in React is `React.createElement`.

So, that you understand this, let's see we have the following sentence:

```jsx
<button type="submit">
  Hello
</button>
```

Is transformed to:

```javascript
React.createElement(
  'button',
  { type: 'submit' },
  'Hello'
)
```

That's why we need to import `React` event if we don't use it explicitly!

So, what if we can take control over this transform process? That's exactly a pragma let us. So, let's code it.

{% gist https://gist.github.com/gugadev/9c1697f5718a06eb281b280d267419da %}

So, what we did here? First, we need to get the check if it's an custom element. If is, assign a `ref` callback. Inside this callback we need to handle the logic.

Once inside the `ref` callback, get all the custom events and the complex properties. For the first one, the event handler name must start with the prefix `onEvent` (necessary to not conflict with regular JSX events). For the properties, we are going to check if the type is an object (typeof).

```javascript
/** Map custom events as objects (must have onEvent prefix) */
const events =
Object
  .entries(props)
  .filter(([k, v]) => k.match(eventPattern))
  .map(([k, v]) => ({ [k]: v }))
/** Get only the complex props (objects and arrays) */
const complexProps =
Object
  .entries(props)
  .filter(([k, v]) => typeof v === 'object')
  .map(([k, v]) => ({ [k]: v }))
```

At this point, we have both, the custom event handlers and the complex properties. The next step is iterate the event handlers and the complex properties.

```javascript
for (const event of events) {
  const [key, impl] = Object.entries(event)[0]
  const eventName = toKebabCase(
    key.replace('onEvent', '')
  ).replace('-', '')

  /** Add the listeners Map if not present */
  if (!element[listeners]) {
    element[listeners] = new Map()
  }
  /** If the listener hasn't be attached, attach it */
  if (!element[listeners].has(eventName)) {
    element.addEventListener(eventName, impl)
    /** Save a reference to avoid listening to the same value twice */
    element[listeners].set(eventName, impl)
    delete newProps[key]
  }
}
```

For each event handler, we need to:

- convert the camel case name to kebab case: Eg. `onEventToggleAccordion` to `toggle-accordion`.
- Add the event handler to the listeners map to remove the listener later.
- add the listener to the custom element.

For the properties is pretty similar and simple:

```javascript
for (const prop of complexProps) {
  const [key, value] = Object.entries(prop)[0]
  delete newProps[key]
  element[key] = value // assign the complex prop as property instead attribute
}
```

Finally, call the `React.createElement` function to create our element:

```javascript
return React.createElement.apply(null, [type, newProps, ...children])
```

And that's all. Now, just left use it.

## Using the custom JSX pragma

There are two ways of using a custom pragma. The first is through the `tsconfig.json`:

```json
{
  "compilerOptions": {
    "jsx": "react",
    "jsxFactory": "webComponents" // default is "React.createElement"
  }
}
```

The second one is through a comment at top of the files:

```javascript
/** @jsx webComponents */
```

Any of these two options you use need to import our pragma:

```javascript
import React from 'react'
import webComponents from '../lib/ce-pragma' // our pragma

// our component
```

Now, you can use your custom elements as any regular React component:

```jsx
/** @jsx webComponents */
import { SFC } from 'react'
import webComponents from '../lib/ce-pragma'

export default function MyScreen() {
  const handleCustomEvent = function (e) {

  }
  return (
    <div>
      <my-custom-component
        data={[ { a: true} ]}
        onMyCustomEvent={handleCustomEvent}
      />
    </div>
  )
}
```

---

## Conclusion

Using a custom pragma sounds like a very situable solution for now. Maybe in a short-term future React have better custom elements support. All could be possible in the crazy and big JavaScript ecosystem.