RSX is a component model where rendering is explicit.
No hooks. No effects. No hidden re-renders.
You decide when UI updates happen.
On-Demand Rendering
Rendering happens only when you call it.
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.
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.
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
});
}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
Add the plugin to your Babel configuration. Choose the setup that matches your bundler:
// 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
}),
],
});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",
},
],
},
};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.
For RSX-specific linting and recommended VS Code lint configuration, use the official plugin:
https://github.com/LMS007/eslint-plugin-rsx
eslint.config.jsvscode/settings.json
RSX fully supports TypeScript in .rsx files. Follow these steps to enable it:
This provides:
- Type declarations for
*.rsximports (so TypeScript understandsimport X from "./X.rsx") - The
Ctxtype for typing RSX component parameters
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.
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 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
import Counter from "./Counter.rsx";
function App() {
return (
<div>
<h1>My App</h1>
<Counter name="Count clicks" />
</div>
);
}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.
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.”
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 |
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.
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.
- High‑frequency updates (60fps animations, real‑time data)
- Many instances (100+ timers, particles, list items)
- Complex hook graphs (effects depending on effects)
RSX shines where imperative control beats declarative diffusion.
- 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.
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
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.
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.
| React | RSX |
|---|---|
| State‑driven | Event‑driven |
| Implicit re‑execution | Explicit rendering |
| Hooks manage lifetimes | Code manages itself |
| Optimization via memo | No optimization required |
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
{ "compilerOptions": { "jsx": "react-jsx", "types": ["@lms5400/babel-plugin-rsx/types"] }, "include": ["src/**/*", "src/**/*.rsx"] // Include .rsx files }