Docs

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.

Use with Vite | Use in Next.js | NiralScript language