@lms5400/babel-plugin-rsx
v1.2.0
Published
Babel plugin for RSX Components
Maintainers
Readme
RSX — On‑Demand Rendered Components
RSX is a component model designed for real‑time and imperative workloads where React’s state, effects, and memoization layers become accidental complexity.
RSX = On‑Demand Rendered Components You decide when rendering happens.
RSX components look familiar to React developers, but behave very differently under the hood. They eliminate the need for hooks entirely—including useState, useEffect, useCallback, useMemo, and useRef—by making rendering an explicit operation instead of a side‑effect of state changes.
Why RSX Exists
React excels at declarative UI driven by application state. But many real‑time systems are not state‑driven UIs:
- Timers, clocks, and stopwatches
- Game loops and input polling
- Media processing (audio/video analysis)
- Hardware and device bridges
- Animation engines and render pipelines
- High‑frequency data streams
In these domains, React often forces developers into patterns like:
- Deep
useEffectchains useCallbackfor “stability”useMemoto fight re‑executionuseRefas escape hatches- Logic hidden inside custom hooks
The result is indirect control, harder reasoning, and fragile behavior.
RSX flips this model.
Basic Structure
export default function Example({ view, update, destroy, render, props }) {
// Everything in this scope runs exactly once on
// mount and persists for the duration of the component.
// Initial props snapshot (mount only)
const initialProps = props;
// Persistent state
let value = 0;
function increment() {
value++;
render(); // explicit re-render
}y
view((props) => {
// The render function
return <button onClick={increment}>{value}</button>;
});
update((prevProps, nextProps) => {
// runs when props change
});
destroy(() => {
// runs once on unmount
});
}How to Setup
1. Install the Package
npm install @lms5400/babel-plugin-rsxNote: This plugin requires
@babel/coreand@babel/preset-reactas peer dependencies. Most React projects using Vite or Next.js already have these bundled internally. If you're using Webpack or a custom setup, check if they exist in yournode_modules/. If not, install them:npm install --save-dev @babel/core @babel/preset-react
2. Configure Babel
Add the plugin to your Babel configuration. Choose the setup that matches your bundler:
Option A: Vite (recommended)
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { rsxTypeStripPlugin, rsxVitePlugin } from "@lms5400/babel-plugin-rsx/vite";
export default defineConfig({
resolve: {
extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json", ".rsx"],
},
plugins: [
// Plugin order matters:
rsxTypeStripPlugin(), // 1. Strip TypeScript (optional - only if using TS in .rsx)
rsxVitePlugin(), // 2. RSX → React transformation
react({ // 3. JSX → JS
include: /\.(jsx|tsx)$/ // note: no need to include rsx because rsxVitePlugin already transforms JSX in .rsx files
}),
],
});Option B: Webpack (Recommended for Webpack users)
Use the dedicated RSX loader which handles TypeScript stripping and RSX transformation:
// webpack.config.js
module.exports = {
resolve: {
extensions: [".js", ".jsx", ".ts", ".tsx", ".rsx"],
},
module: {
rules: [
// RSX files - use dedicated loader
{
test: /\.rsx$/,
exclude: /node_modules/,
use: "@lms5400/babel-plugin-rsx/webpack-loader",
},
// Regular JS/TS files
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: "babel-loader",
},
],
},
};Option C: Babel Config (works with any bundler)
Create or update your babel.config.js:
module.exports = {
presets: [
["@babel/preset-react", { runtime: "automatic" }],
"@babel/preset-typescript", // Required for TypeScript in .rsx files
],
plugins: ["@lms5400/babel-plugin-rsx"],
};Note: This approach requires
@babel/preset-typescriptto strip TypeScript before RSX transformation.
3. [Highly Recommended] – Configure ESLint for RSX Files
For RSX-specific linting and recommended VS Code lint configuration, use the official plugin:
https://github.com/LMS007/eslint-plugin-rsx
Files to edit/create:
eslint.config.jsvscode/settings.json
4. [Optional] - Add TypeScript Support
RSX fully supports TypeScript in .rsx files. Follow these steps to enable it:
Step 1: Add RSX types to tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"types": ["@lms5400/babel-plugin-rsx/types"]
},
"include": ["src/**/*", "src/**/*.rsx"] // Include .rsx files
}This provides:
- Type declarations for
*.rsximports (so TypeScript understandsimport X from "./X.rsx") - The
Ctxtype for typing RSX component parameters
Step 2: Configure VS Code
Create or update .vscode/settings.json to treat .rsx files as TypeScript React:
{
"files.associations": {
"*.rsx": "typescriptreact"
}
}This enables full IDE support: syntax highlighting, IntelliSense, error checking, and go-to-definition.
Step 3: Update ESLint for TypeScript (if using ESLint)
If you followed Step 3 above to configure ESLint with @lms5400/eslint-plugin-rsx, update your eslint.config.js to use the TypeScript parser for .rsx files:
import tseslint from "typescript-eslint";
export default [
// ... other configs
{
files: ["**/*.rsx"],
languageOptions: {
parser: tseslint.parser,
parserOptions: {
ecmaFeatures: { jsx: true },
},
},
// ... rules
},
];Create Your First RSX Component
Create a file with the .rsx extension:
Note: The
RSX()wrapper is necessary when mixing TypeScript and RSX. See TS_Semantics.md for the full rationale and a nested components.
// Counter.rsx
import type { RSX } from "@lms5400/babel-plugin-rsx/types";
interface CounterProps {
name: string;
}
export default function RSX<CounterProps>(Counter({ view, render }) {
let count = 0;
function increment() {
count++;
render();
}
view((props) => (
<>
<label>{props.name}</label>
<button onClick={increment}>Count: {count}</button>
</>
));
})The RSX parameter uses the Ctx<P> generic type with the following structure:
props- your component's props (typeP)view(cb)- register view callback, receives(props: P) => ReactNodeupdate(cb)- register update callback, receives(prev: P, next: P) => voidrender()- trigger a re-renderdestroy(cb)- register cleanup callback
Use It in Your React App
import Counter from "./Counter.rsx";
function App() {
return (
<div>
<h1>My App</h1>
<Counter name="Count clicks" />
</div>
);
}The Core Idea: Render On Demand
In RSX:
- Rendering is explicit
- Logic runs once, not on every re‑render
- Local variables behave like real variables
- You call
render()only when output must change
There is no implicit reactivity.
If nothing meaningful changed, nothing renders.
This matches how real‑time systems already work.
Why RSX Needs No Hooks (Including memo)
Hooks exist to compensate for React’s re‑execution model:
| React Hook | Why It Exists |
| ------------- | ----------------------------- |
| useState | Triggers renders indirectly |
| useEffect | Run code after render |
| useCallback | Prevent identity churn |
| useMemo | Prevent recomputation |
| useRef | Persist values across renders |
RSX removes the root cause:
- The component function does not re‑execute on updates
- Variables persist naturally
- Side‑effects are just normal code
- All data can be mutated
- Updates are intentional
Because nothing re‑runs implicitly:
memois unnecessarycallbackstability is irrelevant- dependency arrays disappear
Mutating data in RSX is safe because the framework does not rely on immutability or identity checks to decide when to update the UI. You explicitly control when rendering happens, so there’s no hidden scheduling, diffing, or replay that could be confused by in-place changes and therefore there is nothing to “optimize around.” or “safeguard.”
Why render() in RSX Is Faster
When you call render() in RSX, you skip the overhead that React incurs on every update:
| React (on every render) | RSX (on render()) |
| -------------------------------------------- | ---------------------------------- |
| Re‑executes entire component function | Only re‑runs the view() callback |
| Runs all hooks sequentially | No hooks to run |
| Compares dependency arrays (useMemo, etc.) | No dependency tracking |
| Recreates closures and inline functions | Functions created once, persist |
| Checks memo wrappers for prop changes | No memo wrappers needed |
| Schedules effects, flushes effect cleanup | Side‑effects are imperative code |
| Data needs to be copied for immutability | No need to copy, just mutate in place |
The Real Cost of Hooks
In React, even a "simple" component with a few hooks pays these costs every render:
function ReactTimer() {
const [time, setTime] = useState(0); // hook 1
const intervalRef = useRef(null); // hook 2
const start = useCallback(() => { ... }, []); // hook 3 + dep check
const stop = useCallback(() => { ... }, []); // hook 4 + dep check
useEffect(() => { ... }, [time]); // hook 5 + dep check + cleanup
return <div>{time}</div>;
}Every frame: 5 hook calls, 3 dependency array comparisons, potential effect scheduling.
RSX Equivalent
function RsxTimer({ view, render }) {
let time = 0;
let intervalId = null;
function start() {
intervalId = setInterval(() => {
time++;
render();
}, 1000);
}
function stop() {
clearInterval(intervalId);
}
view(() => <div>{time}</div>);
}On render(): just the view() callback runs. No hook overhead. No comparisons.
The performance gap widens with:
- High‑frequency updates (60fps animations, real‑time data)
- Many instances (100+ timers, particles, list items)
- Complex hook graphs (effects depending on effects)
What Types of React Components Are Perfect for RSX conversion?
RSX shines where imperative control beats declarative diffusion.
Ideal Use Cases
- Timers & schedulers
- Animation loops (
requestAnimationFrame) - Gamepad, MIDI, HID, or sensor input
- Audio / video analysis
- Streaming or polling systems
- Canvas / WebGL / WebGPU renderers
- Electron IPC bridges
- High‑frequency UI updates
If a component:
- Does work continuously
- Talks to hardware or external systems
- Maintains internal mutable state
- Uses many
useRefsas escape hatches anduseCallbacksfor stabilization - Should not re‑run on every parent render
- Has tangled or deeply nested
useEffectchains
…it’s likely a strong RSX candidate.
Designed to Be Used With React
RSX is not a replacement for React.
It is meant to be sprinkled into existing JSX projects:
- Use React for layouts, routing, forms, and data fetching
- Use RSX for hot paths and real‑time subsystems
import ReactComponent from 'ReactComponent.tsx'
import RsxComponent from 'RsxComponent.rsx'
...
<div>
<ReactComponent name={hello world}/>
<RsxComponent name={hello world}/>
</div>RSX components:
- Mount inside normal React trees
- Coexist with JSX components
- Do not affect React’s mental model elsewhere
Works Natively with TypeScript
RSX supports TypeScript end‑to‑end:
- Typed props
- Typed local state
- Typed helpers and APIs
- Full IDE inference
Because RSX avoids hook indirection:
- Types are flatter
- Control flow is obvious
- Fewer generics and wrapper types
The result is clearer typings with less ceremony.
Easier to Read, Easier for AI to Write
RSX code is:
- Linear
- Explicit
- Single‑pass
There are no hidden lifecycles, no dependency arrays, and no hook rules.
This makes RSX:
- Easier for humans to reason about
- Far less error‑prone when generated by AI
AI systems struggle with:
- Hook ordering rules
- Dependency correctness
- Memoization correctness
- Effect timing
RSX removes these failure modes entirely.
What you see is what runs.
Mental Model Summary
| React | RSX | | ---------------------- | ------------------------ | | State‑driven | Event‑driven | | Implicit re‑execution | Explicit rendering | | Hooks manage lifetimes | Code manages itself | | Optimization via memo | No optimization required |
When to Avoid RSX
RSX is not a general replacement for React. Prefer JSX + hooks when:
- The component is mostly declarative UI (forms, lists, layout, content)
- UI is derived from app or server state
- Updates are infrequent or user-driven
- The component is meant to be highly composable or generic
- React’s conventions and consistency are more important than control
