souporserious

Reading Context in Pragmas

3 min read

With their rise in popularity from libraries like Emotion, pragmas are a powerful way to augment every call site of JSX. This post will look at how we can build our own pragma that utilizes a Theme Context.

A quick precursor to pragmas if you’ve never used or heard of them. Since JSX is sugar on top of JavaScript, once it is run through a build step, usually with Babel or TypeScript, we transform all of our JSX calls to functions for the respective platform:

1const element = <h1>Hello World</h1>
2
3// is transformed into ->
4
5const element = React.createElement('h1', null, 'Hello world')

This is where we can introduce pragmas. We can augment what function we want to use when transforming our JSX. Rather than setting up a custom pragma for your whole codebase, the easiest way to play with pragmas is using a @jsx comment at the top of the file that points to the function you want to use:

1/** @jsx jsx */
2import React from 'react'
3
4function jsx(type, props, ...children) {
5 return React.createElement(type, props, ...children)
6}
7
8export default function App() {
9 return (
10 <div className="App">
11 <h1>Hello World</h1>
12 <p>Using pragmas for fun and profit.</p>
13 </div>
14 )
15}

This instructs the build tool to use our custom jsx function when compiling JSX calls.

Setting Up Context

When first approaching setting up context in a pragma, I naively did it in the jsx function. While this seemed to work for simple cases, once any conditional logic was introduced, we break the rules of hooks. Digging into other libraries that utilize pragmas, MDX had the most elegant solution to this problem. Simple enough, we can wrap every call site with another element allowing us to use React/Preact as normal:

1/** @jsx jsx */
2import React from 'react'
3
4function CreateElement({ originalType, ...props }) {
5 return React.createElement(originalType, props)
6}
7
8function jsx(type, props, ...children) {
9 return React.createElement(
10 CreateElement,
11 { originalType: type, ...props },
12 ...children
13 )
14}
15
16export default function App() {
17 return (
18 <div className="App">
19 <h1>Hello World</h1>
20 <p>Using pragmas for fun and profit.</p>
21 </div>
22 )
23}

Now we can set up context like we normally would:

1const ThemeContext = React.createContext({})
2
3function CreateElement({ originalType, ...props }) {
4 const themeContext = React.useContext(ThemeContext)
5 return React.createElement(originalType, props)
6}
7
8function jsx(type, props, ...children) {
9 return React.createElement(
10 CreateElement,
11 { originalType: type, ...props },
12 ...children
13 )
14}

Putting it all together, we can now use our theme throughout any JSX props that use our custom pragma:

Conclusion

While pragmas are quite powerful, you should use them with caution. We’re adding functionality on top of potentially every single element, so there could easily be performance issues if you’re not careful. It’s also not practical to do this for theming web apps and may be more suitable for something like React Native.

We only covered adding a pragma for a single page, but there is a handy Babel plugin from the Emotion team to enable this across an entire codebase. Finally, the latest JSX transform recently added an option for auto importing React/Preact createElement calls, which you can read about in detail here. This was beyond this article’s scope, but it is good to know if you are interested in writing your own pragmas.

Updated:
  • development
  • react
  • preact
  • jsx
  • pragma
Previous post
Use Babel to Statically Analyze JSX
Next post
Bundling TypeScript with Esbuild for NPM
© 2022 Travis Arnold