JSX Setup

This document explains how to configure your project to use JSX with rEFui.

Like Solid.js, rEFui is a Retained Mode renderer, but unlike Solid.JS, rEFui does not rely on a specific compiler for JSX, you can just use any existing transpilers and choose between two JSX transformation methods: Classic Transform (preferred) and Automatic Runtime.

Note: For detailed information about rEFui's reactive system and signals, see the Signals documentation.

Classic Transform

This is the recommended approach as it provides the most flexibility, allowing you to use different renderers on a per-component basis. To use it, you need to configure your transpiler (like Babel or esbuild) to use the classic JSX runtime and specify the pragma.

With this setup, your components should be functions that accept a renderer R and return the element tree.

Please note, rEFui does not require JSX injection for the classic transform. The render factory is passed through R in runtime.

Configuration

Bun

In your tsconfig.json, configure compilerOptions based on [tsconfig/jsx](https://www.typescriptlang.org/tsconfig/#jsx)

{
  "compilerOptions": {
    "jsx": "react",
    "jsxFactory": "R.c",
    "jsxFragmentFactory": "R.f",
    ... other options
  }
}

Babel

In your .babelrc.json:

{
  "presets": [
    [
      "@babel/preset-react",
      {
        "runtime": "classic",
        "pragma": "R.c",
        "pragmaFrag": "R.f"
      }
    ]
  ]
}

Vite

In your vite.config.js, configure esbuild's JSX options:

// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  esbuild: {
    jsxFactory: 'R.c',
    jsxFragment: 'R.f',
  },
})

esbuild

Classic pragma via CLI:

esbuild src/main.jsx --bundle --outfile=dist/bundle.js --jsx-factory=R.c --jsx-fragment=R.f

Classic pragma via JS API:

import { build } from 'esbuild'

await build({
	entryPoints: ['src/main.jsx'],
	outfile: 'dist/bundle.js',
	bundle: true,
	jsxFactory: 'R.c',
	jsxFragment: 'R.f',
})

File-level Pragma Comments

If you don't want to configure your build tool, you can add pragma comments to the top of your JSX files:

/** @jsx R.c */
/** @jsxFrag R.f */

// Your component code here

Reflow Renderer

The classic transform ships with a special renderer at refui/reflow (re-exported from refui) that provides a renderer-agnostic JSX runtime. Use it to author pure application logic without binding it to a specific renderer. Prefer exporting pure components from these modules; low-level host elements are available but reduce portability, so keep them inside narrow platform-specific branches.

Reflow is useful when you're sharing logic between multiple platforms that share same basic app logics, so you can focus on these logic without be distracted by nuanced platform specific UI logics, like animations, styling etc.

Historically Reflow required the classic JSX transform, but as of v0.8.0 the automatic runtime also targets it by default. Stick with the classic pragma when you want per-file renderer swapping, or let the automatic runtime wrap your JSX and feed the result to Reflow with zero additional wiring.

Reflow renderer surfaces a module-level R, but the render function still receives its own R argument at render time. The parameter shadows the outer reference, so they do not interfere; only toolchains that mishandle shadowing (notably some SWC-based runtimes) may run into trouble, in which case isolate those modules or swap to the automatic transform without using Reflow mode.

If you construct host nodes manually (or pass through nodes created by another renderer), mark them with markNode from refui/reflow so Reflow recognizes them as nodes. This is especially important when your node type is an array (HTML renderer nodes are arrays); otherwise the JSX automatic runtime may treat the node as an array of children. Use isNode to detect them. The HTML renderer already marks its nodes, so they interoperate out of the box. If you are building your own renderer, make sure nodeOps.isNode recognizes your node shape.

Configure your bundler to inject R like this:

// vite.config.js
import { defineConfig } from 'vite'
import refurbish from 'refurbish/vite'

export default defineConfig({
  plugins: [refurbish()],
  esbuild: {
    jsxFactory: 'R.c',
    jsxFragment: 'R.f',
    jsxInject: `import { R } from 'refui/reflow'`
  }
})

esbuild equivalent:

import { build } from 'esbuild'

await build({
	entryPoints: ['src/main.jsx'],
	outfile: 'dist/bundle.js',
	bundle: true,
	jsxFactory: 'R.c',
	jsxFragment: 'R.f',
	jsxInject: `import { R } from 'refui/reflow'`,
})
// webpack.config.js
import webpack from 'webpack'

export default {
	// ... other configurations
	plugins: [
		// ... other plugins
		new webpack.ProvidePlugin({
			R: ['refui/reflow', 'R']
		})
	]
}
import { signal, useAction, read, watch, onDispose } from 'refui'
// R for Reflow is auto injected by your bundler, or you can do it manually
// import { R } from 'refui/reflow'
// Or import directly from 'refui'
// Or configure JSX automatic runtime for your bundler

const platform = globalThis.RUNTIME_PLATFORM ?? 'browser'

export const CounterDisplay = ({ count }) => {
	if (platform === 'browser') return () => <span>{count}</span>
	if (platform === 'nativescript') return () => <text>{count}</text>
	if (platform === 'cheesedom') return () => <text>{count}</text>

	if (platform === 'breadboard') {
		const display = new LED('max7219')
		watch(() => {
			display.setText(read(count).toString(10))
		})

		onDispose(() => {
			display.close()
		})
	}

	return null
}

export const CounterBtn = ({ onIncrement }) => {
	let lastInc = 0
	const debounced = () => {
		const now = Date.now()
		if (now - lastInc < 100) return
		lastInc = now
		onIncrement()
	}

	if (platform === 'browser') return () => <button on:click={debounced}>+</button>
	if (platform === 'nativescript') return () => <button on:tap={debounced}><text>+</text></button>
	if (platform === 'cheesedom') return () => <object on:clicked={debounced}><text>+</text></object>

	if (platform === 'breadboard') {
		const gpio = GPIO.open('A1', 'input')
		gpio.on('edge_up', onIncrement)

		onDispose(() => {
			gpio.close()
		})
	}

	return null
}

export const App = () => {
	const count = signal(0)
	const [whenIncrement, increment] = useAction()

	whenIncrement(() => {
		count.value += 1
	})

	return (
		<>
			<CounterDisplay count={count} />
			<CounterBtn onIncrement={increment} />
		</>
	)
}

Reflow assumes components stay stateless at declaration time, so inline functions are evaluated immediately and recursively until a non-function value is produced. Treat them as utility helpers; they do not become reactive computations.

Because the helper focuses on render-agnostic logic, $ref bindings only resolve reliably for concrete DOM elements in browser output (and comparable host nodes elsewhere). $ref still works for components, but in v0.8.0+ you should pass per-component expose callbacks to publish handles explicitly. Child components ought to namespace the values they expose to avoid collisions. As component references are not strictly retained in reflow mode, prefer explicit wiring through props and signals where possible.

Expect a small performance overhead when running in development with reflow enabled because the runtime tracks additional metadata, while production builds skip instance allocation and execute functional components as plain functions for better throughput.

Automatic Runtime

This approach uses a single, globally defined renderer. While slightly easier to set up, it is less flexible than the classic transform.

Since v0.8.0, both refui/jsx-runtime and refui/jsx-dev-runtime automatically bind to the Reflow renderer, so you can start writing JSX as soon as your bundler is configured—no manual wrap() call required. Use this mode when targeting toolchains like MDX, SWC, or runtimes such as Deno that cannot inject the classic pragma.

Setup

To use it, you first need to configure your build tool (like Bun, Vite, Rollup with Babel, or Webpack) to use rEFui's runtime.

Bun

In your tsconfig.json, configure compilerOptions based on [tsconfig/jsx](https://www.typescriptlang.org/tsconfig/#jsx)

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "refui",
    ... other options
  }
}

Vite (vite.config.js)

import { defineConfig } from 'vite'

export default defineConfig({
	esbuild: {
		jsx: 'automatic',
		jsxImportSource: 'refui', // This tells Vite/esbuild where to find the runtime
	},
})

esbuild

Automatic runtime via CLI:

esbuild src/main.jsx --bundle --outfile=dist/bundle.js --jsx=automatic --jsx-import-source=refui

Automatic runtime via JS API:

import { build } from 'esbuild'

await build({
	entryPoints: ['src/main.jsx'],
	outfile: 'dist/bundle.js',
	bundle: true,
	jsx: 'automatic',
	jsxImportSource: 'refui',
})

Babel (.babelrc.json)

{
	"presets": [
		[
			"@babel/preset-react",
			{
				"runtime": "automatic",
				"importSource": "refui"
			}
		]
	]
}

Rendering

With the runtime already pointing at Reflow, you just create whichever host renderer you want to mount with and call its render method. No additional setup is necessary for .jsx / .tsx modules.

Because Reflow is renderer-agnostic, component bodies authored in automatic-mode JSX can inline host tags without explicitly returning render factories—the runtime wraps them for you. Just ensure the host renderer you eventually mount (e.g. the DOM renderer) provides implementations for the tags you're emitting.

Example (main.js):

import { createDOMRenderer } from 'refui/dom'
import { defaults } from 'refui/browser'
import App from './App.jsx' // Your root component

const R = createDOMRenderer(defaults)
const root = document.getElementById('app')

R.render(root, App)

Need to override the renderer globally (for example, to plug in a custom host)? Call wrap(newRenderer) explicitly and both the production and dev runtimes will switch away from Reflow.

Hot Module Replacement

For development, use refurbish HMR plugin to provide fast, reliable hot module replacement for your components.

Refurbish is specifically designed for rEFui's component model and handles the complexity of HMR automatically. You don't need to add import.meta.hot checks or other HMR boilerplate to your components.

Quick Setup

Install refurbish:

npm install -D refurbish

For Bun, add to your bunfig.toml:

[serve.static]
plugins = ["refurbish/bun"]

For vite, add to your vite.config.js:

import { defineConfig } from 'vite';
import { refurbish } from 'refurbish/vite';

export default defineConfig({
	plugins: [refurbish()]
});

That's it! Your rEFui components will now hot-reload during development, preserving component state and providing instant feedback.

HMR Best Practices

Learn More: Visit the refurbish repository for Webpack configuration and advanced options.