React-flavored framework inspired by Marko's compiler-first philosophy.
The codebase was largely generated by an AI agent, it may be slop. Good luck.
- Fine-grained reactivity. Update only the DOM nodes whose dependencies changed instead of re-rendering whole component subtrees.
- Modular runtime. Avoid shipping client runtime for routes that do not need it. A route with no client-side signals can stay server-rendered without a client route bundle.
- Compile-time over runtime. When a transformation can be done once by the compiler, it should not be re-done on every render in the browser.
- Chunk-based streaming SSR. HTML is written into a coalescing UTF-8 buffer and flushed at shell, threshold, and out-of-order fragment boundaries. The streaming path is designed to avoid buffering the full rendered document in common cases, and to let bytes leave the server as soon as they are ready, similar to Marko's streaming model.
- Route-level client/runtime inference. The router/compiler uses file conventions such as
.client.tsxand.compat.tsx, configuredclientBoundaryImports, and supported app-local static relative imports to decide which route modules need client runtime. It records inferred client references in server transform metadata and transports that manifest to the hydration runtime. This is not general semantic proof over arbitrary code. - Dual environment optimization. Server and client get separately compiled, separately optimized programs from one source: HTML string/stream emission on the server, fine-grained DOM mutation on the client.
Experimental. APIs may change.
Create an app-router project:
npx @reckona/create-mreact-app my-app --template app-router --src-dir
cd my-app
pnpm install
pnpm devCreate the Tailwind CSS template instead:
npx @reckona/create-mreact-app my-app --template app-router-tailwind --src-dirAdd generic container deploy files for Cloud Run, AWS App Runner, and similar platforms:
npx @reckona/create-mreact-app my-app --template app-router --src-dir --deploy containerGenerate an AWS Lambda entrypoint instead:
npx @reckona/create-mreact-app my-app --template app-router --src-dir --deploy aws-lambdaGenerate a Cloudflare Workers-oriented template:
npx @reckona/create-mreact-app my-app --template cloudflareCloudflare builds emit .mreact/cloudflare/route-modules.mjs for dynamic and non-prerendered App Router pages, and the generated worker imports that registry directly. Use mreact-router build --target=cloudflare for Workers-only artifacts, or mreact-router build --target=node for Node, container, and AWS Lambda artifacts that should not bundle Cloudflare route modules. Generated Cloudflare route modules support stream = true pages with route-local <Await> boundaries and local server-component imports.
Build and run production output:
pnpm build
pnpm startEnable compact request logs in either local development or built-output serving with mreact-router dev --log=requests, mreact-router start .mreact --log=requests, or MREACT_ROUTER_LOG=requests.
Server-rendered routes stay JavaScript-free by default. When a server-only route needs Link prefetch/navigation behavior without hydration, export navigationRuntime = true from that page:
import { Link } from "@reckona/mreact-router/link";
export const navigationRuntime = true;
export default function Page() {
return <Link href="/docs" prefetch="viewport">Docs</Link>;
}The generated Vite config is explicit about app paths:
// vite.config.ts
import { defineConfig } from "vite";
import { mreactRouter } from "@reckona/mreact-router/vite";
export default defineConfig({
plugins: [
mreactRouter({
projectRoot: __dirname,
routesDir: "src/app",
publicDir: "public",
allowedSourceDirs: ["src"],
}),
],
});A typical project layout:
my-app/
src/
app/
layout.tsx
page.tsx
lib/
app-info.ts
public/
vite.config.ts
Routes live under app/ or src/app/. The router recognizes page.tsx, layout.tsx, template.tsx, loading.tsx, error.tsx, not-found.tsx, middleware.ts, and route.ts.
src/app/
layout.tsx
page.tsx
counter/page.tsx
users/$id/page.tsx
files/$...path/page.tsx
api/time/route.ts
// src/app/page.tsx
export const metadata = {
title: "Home",
description: "A server-rendered mreact page.",
};
export default function Page() {
return (
<main>
<h1>Hello from mreact</h1>
<p>This route has no client state, so it can render as static HTML.</p>
</main>
);
}Layouts wrap child routes with <Slot />. Named slots let a child route fill a specific region of a parent layout.
// src/app/docs/layout.tsx
export const metadata = {
title: "Docs",
description: "Documentation section.",
};
export default function DocsLayout() {
return (
<section class="docs-layout">
<aside>
<nav>
<a href="/docs">Overview</a>
<a href="/docs/routing">Routing</a>
</nav>
<Slot name="aside" />
</aside>
<article>
<Slot />
</article>
</section>
);
}// src/app/docs/page.tsx
function TipAside() {
return <p>Read the routing guide next.</p>;
}
export const slots = {
aside: TipAside,
};
export default function Page() {
return <h1>Docs overview</h1>;
}For TypeScript apps that type-check route files directly, include the app-router global declarations so <Slot /> is available without a local import:
{
"compilerOptions": {
"types": ["@reckona/mreact-router/app-router-globals"]
}
}Server-side route code that runs through loaders, middleware, route handlers, metadata, or server actions should use relative imports for app-local modules. The production server bundler applies the import policy before Vite-only or tsconfig path alias plugins can rewrite aliases such as ~/*, so an alias like ~/lib/csrf is treated as a package name by the import policy. Prefer ../lib/csrf.js or another relative specifier in server-side modules.
cell() values are tracked by the compiled client output. A route using cell() and an event handler gets a client route bundle.
For component boundaries, use the file conventions intentionally: .client.tsx
marks a component as a client boundary and .compat.tsx marks React-compatible
component code. The router can infer some route-level client runtime needs from
supported syntax and app-local static imports, but it is not a general semantic
or TypeScript type-flow analyzer. When a server route imports a client component,
render that imported binding as JSX so the analyzer can include it in the
client reference manifest.
Automatic client boundary inference currently follows direct JSX, JSX member roots, simple component aliases, and app-local barrel re-exports:
import { Counter } from "./Counter.client";
import { widgets } from "./widgets.client";
import { SaveButton } from "./components";
const InlineCounter = Counter;
export default function Page() {
return (
<>
<Counter />
<widgets.Toggle />
<InlineCounter />
<SaveButton />
</>
);
}The analyzer cannot prove dynamic registries, computed component selection, or
non-JSX uses are safe client boundaries. In those cases the build emits
MR_CLIENT_BOUNDARY_INFERENCE_UNSUPPORTED_REFERENCE; render the imported
binding through one of the supported JSX shapes or configure
clientBoundaryImports explicitly.
// src/app/counter/page.tsx
import { cell } from "@reckona/mreact-reactive-core";
export default function CounterPage() {
const count = cell(0);
return (
<button type="button" onClick={() => count.set((value) => value + 1)}>
Count: {count.get()}
</button>
);
}The app router intercepts same-origin anchors and updates the changed route payload without a full document reload. It keeps head metadata and route data synchronized, restores scroll for back/forward navigation, and prefetches client route scripts for likely navigations when the browser is not in reduced-data mode.
Use Link or linkProps() when a route needs explicit navigation behavior:
// src/app/page.tsx
import { Link } from "@reckona/mreact-router/link";
export default function Page() {
return (
<nav>
<Link href="/docs" prefetch="viewport">
Docs
</Link>
<Link href="/editor" scroll="preserve" transition="auto">
Editor
</Link>
<Link href="/legacy" reload>
Legacy page
</Link>
</nav>
);
}The client runtime also exposes getNavigationState() and subscribeNavigationState() from @reckona/mreact-router/navigation-state for devtools and advanced UI integrations that need to observe pending navigations. The root @reckona/mreact-router entrypoint still re-exports these helpers for compatibility, but the subpaths are preferred for client-only code because they give bundlers a narrower module boundary.
Use $name for dynamic segments and $...name for catch-all segments. loader() runs before render and passes its return value as props.data.
// src/app/users/$id/page.tsx
import { notFound } from "@reckona/mreact-router";
interface LoaderContext {
params: { id: string };
request: Request;
}
const users = new Map([
["ada", { name: "Ada Lovelace", role: "admin" }],
["grace", { name: "Grace Hopper", role: "editor" }],
]);
export const prerender = true;
export async function generateStaticParams() {
return [...users.keys()].map((id) => ({ id }));
}
export async function loader(context: LoaderContext) {
const user = users.get(context.params.id);
if (user === undefined) notFound();
return user;
}
export default function UserPage(props: {
params: { id: string };
data: { name: string; role: string };
}) {
return (
<main>
<h1>{props.data.name}</h1>
<p>Role: {props.data.role}</p>
</main>
);
}// src/app/files/$...path/page.tsx
export default function FilePage(props: { params: { path: string } }) {
return <p>Requested file path: {props.params.path}</p>;
}Catch-all params are decoded before they reach your route module. Re-encode each segment before embedding them into a URL.
function hrefForCatchAll(path: string): string {
return `/files/${path.split("/").map(encodeURIComponent).join("/")}`;
}A loader can also return a Response to short-circuit page rendering. This is useful for redirects that need headers such as Set-Cookie:
// src/app/login/page.tsx
export function loader() {
return new Response(null, {
status: 303,
headers: {
location: "/",
"set-cookie": "pending_oidc=1; Path=/; HttpOnly; SameSite=Lax",
},
});
}
export default function LoginPage() {
return <main>Login</main>;
}Page modules may export route convention values such as loader, metadata, revalidate, stream, prerender, generateStaticParams, and slots. Other lowercase named exports are treated as local helpers during page rendering, so they can be tested from source without being compiled as route components. Uppercase exported functions are still treated as renderable component helpers.
route.ts files expose HTTP method functions.
// src/app/api/time/route.ts
export function GET(): Response {
return Response.json({
now: new Date().toISOString(),
});
}
export async function POST(request: Request): Promise<Response> {
const body = await request.json();
return Response.json({ received: body });
}Dynamic route handlers receive decoded file-system route params as their second argument:
// src/app/api/users/$id/route.ts
export function GET(
_request: Request,
context: { params: { id: string } },
): Response {
return Response.json({ id: context.params.id });
}Route handlers may return a Response or throw a Response. Throwing a Response is useful for guard helpers that need to stop execution immediately:
function requireCsrf(request: Request): void {
if (request.headers.get("x-csrf") !== "expected") {
throw new Response("CSRF verification failed", { status: 403 });
}
}
export function POST(request: Request): Response {
requireCsrf(request);
return Response.json({ ok: true });
}Middleware can return a Response to short-circuit rendering, or return undefined to continue.
// src/app/middleware.ts
import { redirect } from "@reckona/mreact-router";
export const config = {
matcher: ["/blocked", "/admin/:path*"],
};
export async function middleware(request: Request): Promise<Response | undefined> {
const url = new URL(request.url);
if (url.pathname === "/blocked") {
return new Response("<h1>Blocked</h1>", {
headers: { "content-type": "text/html; charset=utf-8" },
status: 451,
});
}
if (url.pathname.startsWith("/admin")) {
const signedIn = request.headers.get("cookie")?.includes("sid=");
if (!signedIn) redirect("/login");
}
return undefined;
}Streaming routes can flush the shell while async work continues. A collocated loading.tsx file supplies the loading boundary.
// src/app/streaming/page.tsx
export const stream = true;
async function readFeed(): Promise<string[]> {
await new Promise((resolve) => setTimeout(resolve, 100));
return ["Compiler output", "Streaming shell", "Out-of-order fragment"];
}
export default function Page() {
const feed = readFeed();
return (
<main>
<h1>Streaming</h1>
<Await value={feed} placeholder={<p>Loading feed...</p>}>
{(items) => (
<ul>
{items.map((item) => <li key={item}>{item}</li>)}
</ul>
)}
</Await>
</main>
);
}// src/app/streaming/loading.tsx
export default function Loading() {
return <p>Preparing the stream...</p>;
}Server actions currently require a top-level "use server" directive in the action module. The router only lowers imported functions from marked modules when it sees <form action={action}>; this keeps ordinary imported functions out of the server-action registry. Cached route HTML can be invalidated with revalidatePath().
Server action requests reject Content-Length values over 10 MiB by default before parsing FormData or JSON. Pass serverActions: { maxBodyBytes } to the dev server, production server, Vite plugin, or deployment adapter when an app needs a different limit.
// src/app/server-actions/page.tsx
import { addNote } from "./actions.js";
import { listNotes } from "./store.js";
export const revalidate = 30;
export default function Page() {
return (
<main>
<form method="post" action={addNote}>
<input name="text" required maxlength="200" />
<button type="submit">Add note</button>
</form>
<ul>
{listNotes().map((note) => (
<li key={note.id}>{note.text}</li>
))}
</ul>
</main>
);
}// src/app/server-actions/actions.ts
"use server";
import { revalidatePath } from "@reckona/mreact-router";
import { addNoteToStore } from "./store.js";
export async function addNote(formData: FormData): Promise<void> {
const raw = formData.get("text");
const text = typeof raw === "string" ? raw.trim() : "";
if (text.length > 200) {
throw new Error("Note text must be at most 200 characters.");
}
if (text.length > 0) {
addNoteToStore(text);
revalidatePath("/server-actions");
}
}Use runtime cache control when the policy depends on request data:
// src/app/products/$id/page.tsx
import { cacheControl } from "@reckona/mreact-router";
export async function loader() {
cacheControl({
sMaxAge: 60,
staleWhileRevalidate: 300,
});
return { generatedAt: new Date().toISOString() };
}@reckona/mreact-query provides a tiny query client. The router gives loaders a per-request QueryClient; after render it dehydrates the cache into HTML.
// src/app/query/page.tsx
import {
createQuery,
getQueryClient,
type QueryClient,
} from "@reckona/mreact-query";
const TIME_KEY = ["time"] as const;
async function fetchTime() {
return { value: new Date().toISOString() };
}
export async function loader(context: { queryClient: QueryClient }) {
return context.queryClient.fetchQuery({
queryKey: TIME_KEY,
queryFn: fetchTime,
});
}
export default function Page(props: { data: { value: string } }) {
const query = createQuery(getQueryClient(), {
queryKey: TIME_KEY,
queryFn: fetchTime,
});
const result = query.result.get();
return (
<main>
<p>Loader value: {props.data.value}</p>
<p>Reactive value: {result.data?.value ?? "pending"}</p>
</main>
);
}@reckona/mreact-forms keeps form state in reactive cells and can map server validation errors back to fields.
// src/app/contact/page.tsx
import { createForm } from "@reckona/mreact-forms";
interface ContactValues {
email: string;
message: string;
}
export default function Page() {
const form = createForm<ContactValues>({
initialValues: { email: "", message: "" },
validateOn: ["change", "blur"],
validate: {
email: (value) => value.includes("@") ? undefined : "Enter a valid email.",
message: (value) => value.length >= 10 ? undefined : "Write at least 10 characters.",
},
});
async function submit() {
const result = await form.submit(async (values) => {
const response = await fetch("/api/contact", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(values),
});
if (!response.ok) {
form.setServerErrors(await response.json());
return;
}
form.reset();
});
if (result.status === "error") {
form.setErrors({ root: ["Could not submit the form."] });
}
}
const state = form.state.get();
return (
<form onSubmit={(event) => { event.preventDefault(); void submit(); }}>
<input
value={state.values.email}
onInput={(event) =>
void form.setValue("email", (event.target as HTMLInputElement).value)}
/>
<textarea
value={state.values.message}
onInput={(event) =>
void form.setValue("message", (event.target as HTMLTextAreaElement).value)}
/>
<button type="submit" disabled={state.submitting}>Send</button>
</form>
);
}@reckona/mreact-auth builds on router sessions and adds role/permission guards.
// src/app/session-store.ts
import {
configureAuth,
createMemorySessionStore,
} from "@reckona/mreact-auth";
export interface SessionData {
userId: string;
roles: string[];
}
export const sessions = createMemorySessionStore<SessionData>();
configureAuth({
redirectTo: "/login",
forbiddenTo: "/forbidden",
});// src/app/admin/audit/page.tsx
import { requireRole } from "@reckona/mreact-auth";
import { sessions, type SessionData } from "../../session-store.js";
export async function loader(context: { request: Request }) {
const session = await requireRole<SessionData>(
context.request,
sessions,
"admin",
);
return { userId: session.data.userId };
}
export default function Page(props: { data: { userId: string } }) {
return <h1>Audit log for {props.data.userId}</h1>;
}defineMessages() keeps message bundles typed. detectLocale() can read from the URL prefix or Accept-Language.
// src/app/i18n/messages.ts
import { defineMessages } from "@reckona/mreact-router";
export const messages = defineMessages({
en: { heading: "Locale detection", welcome: "Hello!" },
ja: { heading: "Locale detection (ja)", welcome: "Hello from ja!" },
});// src/app/i18n/page.tsx
import { detectLocale } from "@reckona/mreact-router";
import { messages } from "./messages.js";
export function loader(context: { request: Request }) {
return detectLocale(context.request, {
defaultLocale: "en",
locales: ["en", "ja"],
});
}
export default function Page(props: { data: { locale: "en" | "ja" } }) {
const t = messages[props.data.locale];
return <h1>{t.heading}</h1>;
}The router provides adapters for common deployment shapes:
// Node http server
import { createNodeRequestHandler } from "@reckona/mreact-router/adapters/node";
const handler = createNodeRequestHandler({
allowedHosts: ["example.com"],
hostPolicy: "strict",
onResponse(response) {
response.headers.set("strict-transport-security", "max-age=31536000; includeSubDomains");
response.headers.set("x-content-type-options", "nosniff");
response.headers.set("referrer-policy", "same-origin");
},
outDir: ".mreact",
port: 3000,
});For public deployments, set allowedHosts to the exact hosts your app serves.
Use hostPolicy: "strict" to fall back to the configured hostname/port when a
request Host is not allow-listed. Use hostPolicy: "trusted-proxy" only when a
trusted reverse proxy normalizes the Host header before traffic reaches mreact.
Use onResponse to add global headers to the final Response; it runs for rendered pages, route handlers, middleware responses, redirects, errors, prerendered routes, and built static/client assets returned through the built app runtime.
// Edge-style runtime
import { createEdgeRequestHandler } from "@reckona/mreact-router/adapters/edge";
const handler = createEdgeRequestHandler({
render(request) {
const url = new URL(request.url);
return new Response(`<h1>${url.pathname}</h1>`, {
headers: { "content-type": "text/html; charset=utf-8" },
});
},
});
export default {
fetch(request: Request) {
return handler(request);
},
};Additional adapters are available at:
@reckona/mreact-router/adapters/aws-lambda@reckona/mreact-router/adapters/cloudflare@reckona/mreact-router/adapters/static
// AWS Lambda HTTP API v2 / Lambda Function URL
import { createAwsLambdaRequestHandler } from "@reckona/mreact-router/adapters/aws-lambda";
export const handler = createAwsLambdaRequestHandler({
onResponse(response) {
response.headers.set("x-frame-options", "DENY");
response.headers.set("permissions-policy", "camera=(), microphone=(), geolocation=()");
},
outDir: ".mreact",
importPolicy: {
allowedPackages: [
"@reckona/mreact",
// "cookie",
// "zod",
],
},
});Production adapters enforce the app-router import policy when bundling loaders, middleware, route handlers, metadata, and server actions. Add every npm package imported by server-side application code to importPolicy.allowedPackages, including dependencies reached through app-local helper modules.
For Lambda Function URL response streaming, use the explicit streaming handler:
import { createAwsLambdaStreamingRequestHandler } from "@reckona/mreact-router/adapters/aws-lambda";
export const handler = createAwsLambdaStreamingRequestHandler({
outDir: ".mreact",
});create-mreact-app --deploy container generates a vendor-neutral Dockerfile, .dockerignore, and docs/deploy/container.md. The generated image uses Node 24 LTS, sets PORT=8080, builds with mreact-router build --target=node, and starts with mreact-router start .mreact through the package start script.
The same container shape works for Cloud Run, AWS App Runner, Fly.io, Render, and other platforms that run an HTTP server from a container:
FROM node:24-bookworm-slim AS deps
WORKDIR /app
RUN corepack enable
COPY . .
RUN pnpm install --frozen-lockfile || pnpm install
FROM node:24-bookworm-slim AS build
WORKDIR /app
RUN corepack enable
COPY --from=deps /app ./
RUN pnpm run build
FROM node:24-bookworm-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=8080
RUN corepack enable
COPY --from=build /app/package.json ./package.json
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/.mreact ./.mreact
EXPOSE 8080
CMD ["pnpm", "start"]create-mreact-app --deploy aws-lambda generates src/lambda.ts and docs/deploy/aws-lambda.md. The generated handler targets API Gateway HTTP API v2 and Lambda Function URL payload format 2.0:
import { createAwsLambdaRequestHandler } from "@reckona/mreact-router/adapters/aws-lambda";
export const handler = createAwsLambdaRequestHandler({
outDir: new URL("../.mreact", import.meta.url).pathname,
importPolicy: {
allowedPackages: [
"@reckona/mreact",
// "cookie",
// "zod",
],
},
});Add every npm package imported by loaders, middleware, route handlers, metadata, server actions, or their app-local helper modules to importPolicy.allowedPackages. Common examples include validation, cookie, database, auth, and AWS SDK packages.
Use relative imports for app-local server modules in Lambda builds. TypeScript or Vite path aliases such as ~/* are not resolved before the production import policy checks package imports.
Build Lambda artifacts with mreact-router build --target=node so the build writes only the Node-compatible server/client output and does not attempt to bundle Cloudflare Workers route modules for loaders that import Node-only dependencies such as database drivers.
Keep Lambda assets below AWS's 250 MB unzipped deployment package limit by packaging a dedicated directory instead of the project root. The runtime needs .mreact/, the bundled Lambda handler such as dist/lambda.mjs, package.json / lockfiles, and production node_modules; src/, tests, dev dependencies, build caches, and Playwright/Vitest/Vite tooling are not required at runtime. The Lambda adapter treats outDir as read-only and materializes generated runtime files under /tmp/mreact-router/<hash>/runtime by default, with a node_modules symlink back to the deployed package root. Handler creation starts a background preload for the built runtime, loader modules, middleware, route handlers, and route metadata so route-specific bundling can move out of the first matched request on warmable runtimes. Set runtimeDir only when you need a custom writable cache directory. For pnpm, a deploy script can copy those files into .lambda/ and run pnpm --dir .lambda install --prod --frozen-lockfile --ignore-scripts --config.node-linker=hoisted before CDK/SAM/serverless packages that directory. pnpm's default isolated linker creates many symlinks, and some Lambda packaging tools dereference or count those links differently; verify find .lambda -type l | wc -l and the actual file bytes in addition to du -sh .lambda. Packages listed in importPolicy.allowedPackages must also exist in that production dependency set.
Lambda proxy responses are buffered, so this adapter does not provide true response streaming. For production, serve .mreact/client from S3 + CloudFront or another CDN and configure assetBaseUrl / publicAssetBaseUrl.
Use createAwsLambdaStreamingRequestHandler() only with a Lambda Function URL or API Gateway integration configured for payload response streaming. It uses the Node.js Lambda runtime awslambda.streamifyResponse() and awslambda.HttpResponseStream.from() APIs, and streams response bytes without base64 buffering.
Built client route assets are written to .mreact/client. Public files are copied from public/ to .mreact/client/public. By default, the mreact server serves those assets itself:
/_mreact/client/*- root public paths such as
/styles.css
To serve static assets from a CDN, upload .mreact/client to a static origin and configure base URLs in vite.config.ts:
import { defineConfig } from "vite";
import { mreactRouter } from "@reckona/mreact-router/vite";
export default defineConfig({
plugins: [
mreactRouter({
projectRoot: __dirname,
routesDir: "src/app",
publicDir: "public",
allowedSourceDirs: ["src"],
assetBaseUrl: "https://cdn.example.com/_mreact/client/",
publicAssetBaseUrl: "https://cdn.example.com/",
}),
],
});assetBaseUrl is used for route scripts and modulepreload links emitted into HTML. publicAssetBaseUrl is persisted in the server manifest and is intended for public asset helpers and deployment tooling. If these options are omitted, the generated HTML stays on the existing root-relative paths.
Use @reckona/mreact-reactive-core outside the router or inside compiled routes.
import { batch, cell, computed, effect } from "@reckona/mreact-reactive-core";
const first = cell("Ada");
const last = cell("Lovelace");
const fullName = computed(() => `${first.get()} ${last.get()}`);
const dispose = effect(() => {
console.log(fullName.get());
});
batch(() => {
first.set("Grace");
last.set("Hopper");
});
dispose();When you use the low-level DOM bindings directly, mount them through createRoot() from @reckona/mreact-reactive-dom and keep the returned dispose function. Bindings and effects are intentionally explicit-lifetime primitives; if you create them outside a root scope, you must dispose them yourself. Compiled router/client output wires this scope for you.
@reckona/mreact-store wraps reactive state with patch updates, transactions, selectors, subscriptions, and optional persistence/instrumentation hooks.
import { createStore, shallowEqual } from "@reckona/mreact-store";
interface CartState {
lines: Array<{ id: string; quantity: number }>;
promoCode: string | null;
}
const cart = createStore<CartState>({
lines: [{ id: "book", quantity: 1 }],
promoCode: null,
});
const itemCount = cart.select(
(state) => state.lines.reduce((total, line) => total + line.quantity, 0),
);
cart.transaction(() => {
cart.set({ promoCode: "MREACT10" });
cart.update((state) => ({
lines: state.lines.map((line) =>
line.id === "book" ? { ...line, quantity: line.quantity + 1 } : line,
),
}));
});
cart.select((state) => ({ promoCode: state.promoCode }), shallowEqual);
console.log(itemCount.get());@reckona/mreact and @reckona/mreact-dom expose React-like entry points for compatibility-oriented builds.
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@reckona/mreact"
}
}import { Suspense, lazy, useEffect, useState } from "@reckona/mreact";
import { createRoot } from "@reckona/mreact-dom/client";
const LazyPanel = lazy(() => import("./Panel.js"));
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `count = ${count}`;
}, [count]);
return (
<main>
<button type="button" onClick={() => setCount((value) => value + 1)}>
Count: {count}
</button>
<Suspense fallback={<p>Loading...</p>}>
<LazyPanel />
</Suspense>
</main>
);
}
createRoot(document.getElementById("root")!).render(<App />);For source that imports react and react-dom, configure your bundler to resolve those specifiers to the mreact shim packages or use the workspace example in examples/react-compat.
Compatibility scope:
| Area | Status | Notes |
|---|---|---|
JSX runtime, createElement, fragments |
Supported | Covered by the compat compiler/runtime path. |
useState, useReducer, useMemo, useCallback, useRef, useEffect, useLayoutEffect |
Supported | Intended for React-like client components. |
| Context | Supported | Provider, consumer, and useContext are implemented in the compat runtime. |
Suspense, lazy |
Partial | Fallback and retry are implemented; this is not a full React concurrent renderer. |
startTransition, useTransition, useDeferredValue |
Partial | Scheduled through mreact's cooperative scheduler, with a smaller surface than React. |
useSyncExternalStore |
Partial | Snapshot stabilization exists, but React's full tearing semantics are not guaranteed. |
| Portals | Not supported | Avoid APIs that require createPortal. |
| Server Components / Flight client interop | Experimental | Flight encode/decode exists for mreact router internals, not React ecosystem compatibility. |
| React DevTools protocol | Not supported | mreact has its own lightweight devtools hooks. |
@reckona/mreact-server can render compiled server-stream functions to strings or streaming sinks directly.
import { renderToString, type HtmlSink } from "@reckona/mreact-server";
function Page(sink: HtmlSink) {
sink.append("<main>");
sink.append("<h1>Server HTML</h1>");
sink.append("<p>Rendered without the app router.</p>");
sink.append("</main>");
}
const html = await renderToString(Page);For lower-level streaming, pass a sink to a compiled server-stream module:
const chunks: string[] = [];
await Page({
append(chunk: string) {
chunks.push(chunk);
},
});The repository contains two benchmark suites:
- benchmarks/primitive: primitive UI/reactivity comparisons across mreact, React, Solid, Svelte, Qwik, Marko, and beta variants where available.
- benchmarks/router: app-router comparisons across mreact app router, Next.js App Router, Qwik City, SolidStart, TanStack Start, Marko Run, and beta variants where available.
Some compared frameworks, especially Qwik and Marko, are designed around resumability or partial client activation rather than React-style full hydration. These benchmarks include server rendering, streaming, bundle size, and browser interaction timing, but they do not fully model every scenario where delaying or avoiding client-side work is beneficial. Primitive DOM-update cases mostly measure already-active update paths, and the browser interaction cases use small synthetic routes.
For resumability-oriented frameworks, interpret first-interaction and update-path
results together with shipped JavaScript size, startup work, and whether unused UI
needs to be activated at all. The router suite splits browser interaction timing
into initial page load before interaction, first interaction from
DOMContentLoaded, first interaction after networkidle, and second interaction
latency to make those trade-offs easier to read.
Some primitive cases intentionally expose architectural differences. For example,
select row in 10k rows is fast in mreact because the selected state is attached
to the class attributes that actually change, so the update path mutates a small
number of DOM nodes directly. React re-runs reconciliation for the keyed list, and
other fine-grained frameworks vary depending on whether their adapter binds the
selection state directly to DOM attributes or routes it through broader list
bookkeeping.
Run them from the repo root:
pnpm bench:primitive
pnpm bench:router
pnpm bench:allThe latest GitHub Actions benchmark runs are listed on the
Benchmarks workflow page.
Each run uploads primitive.md, router.md, and the corresponding JSON summary
files as artifacts.
The examples/ directory contains focused applications:
| Example | What it demonstrates |
|---|---|
| examples/app-router | Full app-router tour: layouts, metadata, streaming, server actions, cache, route handlers, middleware, auth, query, forms, i18n, deployment adapters |
| examples/reactive-primitives | cell, computed, effect, and DOM updates |
| examples/store | Shared store, selectors, transactions, and subscriptions |
| examples/ssr-streaming | String rendering, streaming rendering, and async boundaries |
| examples/react-compat | React-like hooks, Suspense, lazy, and DOM root entry points |
| examples/selective-hydration | Selective hydration without the app router |
| Package | Purpose |
|---|---|
@reckona/mreact |
React-like public runtime entry point |
@reckona/mreact-dom |
React DOM-compatible client and server entry points |
@reckona/mreact-compat |
Compatibility runtime used by the public React-like packages |
@reckona/mreact-scheduler |
Scheduler compatibility package |
@reckona/mreact-compiler |
TSX compiler for client and server targets |
@reckona/mreact-reactive-core |
cell, computed, effect, batch, and dependency tracking |
@reckona/mreact-reactive-dom |
DOM bindings for text, lists, events, props, and hydration |
@reckona/mreact-server |
SSR string, stream, async boundary, and Flight helpers |
@reckona/mreact-router |
File-system app router, build pipeline, server actions, cache, adapters |
@reckona/mreact-vite |
Standalone Vite plugin for compatibility-oriented builds |
@reckona/mreact-shared |
Shared HTML escaping and URL safety helpers |
@reckona/mreact-query |
Query cache, mutation observer, dehydration, client hand-off |
@reckona/mreact-store |
Global/client state primitives |
@reckona/mreact-auth |
Session and authorization helpers |
@reckona/mreact-forms |
Form validation and server-action error integration |
@reckona/mreact-devtools |
Shared development event hooks |
@reckona/mreact-test-utils |
Router and SSR testing helpers |
@reckona/create-mreact-app |
Project scaffolder |
@reckona/mreact-router-native |
Optional native route matcher package with platform variants |
@reckona/mreact-router-native-linux-x64-gnu |
Linux x64 glibc native addon package |
@reckona/mreact-router-native-darwin-arm64 |
macOS arm64 native addon package |
@reckona/mreact-router-native-win32-x64-msvc |
Windows x64 MSVC native addon package |
@reckona/mreact-next |
Experimental Next-oriented compiler integration |
Install dependencies and build packages:
pnpm install
pnpm buildRun tests:
pnpm test
pnpm test:e2eRun the app-router example:
pnpm example:mreact-app-router:devGenerate and check API documentation:
pnpm docs:api
pnpm docs:api:check
pnpm api:report
pnpm api:report:checkdocs/api contains the generated TypeDoc HTML reference and is intentionally committed. etc/api contains API Extractor reports used to review public API signature changes.
MIT