Use NiralScript in React
Consume compiled NiralScript (.nrs) modules as idiomatic React with the @niral/react adapter: useNrs, bindNrs, live reactive reads, RPCs and prop flow.
On this page 6 sections
.nrs files hold reactive logic — input, state, derive and exported functions — with no markup. In a React app, React owns the markup and the @niral/react adapter turns a compiled .nrs module into live reactive values and callable functions through the useNrs hook.
The adapter is peer-only: it declares react as a peer dependency and ships no bundler or runtime of its own, so your app stays in control of its dependencies.
Install
Requires Node.js 22 or newer and React 18 or newer. You also need a bundler step that can import .nrs files — see Use with Vite or Use in Next.js.
npm install @niral/react
Write a component
Save the reactive logic as counter.nrs:
input initial: number
state count: number = initial
derive doubled: number = count * 2
derive parity: string = count % 2 === 0 ? "even" : "odd"
export function increment(): number {
count += 1
return count
}
export function reset(): void {
count = initial
}
Then render it with React. The .nrs import is resolved by the bundler loader; useNrs returns a live view — reactive reads stay current, exported functions are stable callbacks:
import * as counter from "./counter.nrs";
import { useNrs } from "@niral/react";
export function Counter({ initial = 0 }) {
const c = useNrs(counter, { initial });
return (
<div>
<p>{c.count} (x2 = {c.doubled}, {c.parity})</p>
<button onClick={c.increment}>increment</button>
<button onClick={c.reset}>reset</button>
</div>
);
}
How useNrs works
useNrs(module, props, globals?) creates exactly one NiralScript instance for the lifetime of the mounted component. It subscribes React to the module's reactive revision through useSyncExternalStore, so a state change re-renders only components that read it. Changed props are pushed into the module's input signals after each commit via the module's updateInputs.
- Reactive bindings (
state,derive,input) are read through live getters —
access c.count during render and you always get the current value.
- Exported functions are returned as stable callbacks you can pass to
onClick. - One instance per component means two
<Counter />mounts hold independent state.
useNrs is synchronous and does not support .nrs modules that use top-level await (an async factory). Keep awaited work out of module top level, or resolve it in your React data layer and pass the result in as an input.
Bind a whole component
When a component is just "render this module", bindNrs wraps the hook and a render callback into a standalone component:
import * as counter from "./counter.nrs";
import { bindNrs } from "@niral/react";
export default bindNrs(counter, (c) => (
<button onClick={c.increment}>{c.count} / {c.doubled}</button>
));
Strict types and globals
Enable NiralScript strict checking in your bundler loader (the Vite and Next.js adapters both take { strictTypes: true }). If your module declares host globals, pass their values as the third argument: useNrs(module, props, { now: Date.now() }).
Server-side rendering
useNrs reads the module's initial revision as its SSR snapshot, so react-dom/server renders the initial state deterministically; the client then hydrates and takes over reactivity. This is what the Next.js adapter relies on for Client Components.
