Guides
Server and Client Model
Mreact renders routes on the server by default and only ships browser runtime when a rendered code path needs it. Use this page to decide whether a feature should stay server-only, become client-interactive through inference, use an explicit client boundary, or turn the whole route into a client route.
Server by default
A route with no client behavior can stay JavaScript-free in the browser. Loaders, route handlers, metadata functions, middleware, and server-rendered components run on the server side of the route graph.
When a server-rendered route uses Link from @reckona/mreact-router/link, Mreact can still inject the navigation runtime so link prefetching, scroll behavior, and client navigation work without hydrating the whole route. Use navigationRuntime only when you need to override that automatic behavior.
When code needs the browser
A component path becomes client-interactive when its render path reaches browser behavior such as reactive state, event handlers, browser globals, or a route-level "use client" directive. The surrounding route can still render on the server unless the route module itself opts into browser ownership.
import { cell } from "@reckona/mreact-reactive-core";
export default function CounterPage() {
const count = cell(0);
return (
<main>
<p>{count.get()}</p>
<button onClick={() => count.set((value) => value + 1)}>+1</button>
</main>
);
}Mreact infers common client needs from supported route shapes and app-local static imports. This is not a general semantic or TypeScript type-flow analyzer; use an explicit boundary when the browser work is hidden behind a dynamic registry, indirect dispatch, or another shape the analyzer cannot follow.
Client boundaries
Start with ordinary app-local components. Mreact can infer client interactivity from supported static render shapes, so a page can remain server-rendered while a nested control hydrates in the browser.
// src/app/widgets/LikeButton.tsx
import { cell } from "@reckona/mreact-reactive-core";
export default function LikeButton() {
const likes = cell(0);
return (
<button onClick={() => likes.set((value) => value + 1)}>
{likes.get()} likes
</button>
);
}// src/app/widgets/page.tsx
import { LikeButton } from "./LikeButton.client.js";
export default function WidgetsPage() {
return (
<main>
<h1>Widgets</h1>
<p>This page shell stays server-rendered.</p>
<LikeButton />
</main>
);
}Prefer inference for ordinary counters, menus, and small controls. Use .client.tsx when inference cannot see the browser work, when the boundary is a useful part of the design, or when you are isolating an integration that should be treated as explicitly browser-only.
Reactive update scheduling
cell.set() updates the stored value immediately, invalidates derived values synchronously, and schedules effects and compiler-emitted DOM bindings in a microtask. Reads through .get() see the latest value before that flush runs.
Use batch() for synchronous transactions and batchAsync() only for intentionally scoped async transactions. batchAsync() keeps effect flushing suspended across every awaited step and releases the queued work once the callback resolves or rejects, so avoid wrapping long I/O or user interaction flows.
import { batchAsync, cell, effect } from "@reckona/mreact-reactive-core";
const count = cell(0);
effect(() => {
console.log(count.get());
});
await batchAsync(async () => {
count.set(1);
await Promise.resolve();
count.set(2);
});Route-level "use client"
Use route-level "use client" when the whole page, layout, or template should hydrate as a client route.
"use client";
export default function EditorPage() {
return <main>{startClientOnlyEditor()}</main>;
}This is also the escape hatch when browser behavior is intentionally hidden behind dynamic dispatch and should not rely on inference.
SSR fallback behavior
Client-interactive boundaries still participate in server rendering. Mreact emits a boundary marker and serialized props, and server-renderable children can remain visible in the initial HTML so the page can paint before hydration.
Guard browser globals directly when a component should keep SSR fallback content:
export function CurrentPath() {
if (typeof window === "undefined") {
return <span>Current path unavailable during SSR</span>;
}
return <span>{window.location.pathname}</span>;
}Unguarded browser access, or guards hidden behind aliases the analyzer cannot follow, can produce a placeholder-only boundary. Prefer direct typeof window === "undefined" guards for server-renderable fallback paths.
Choosing the right boundary
- Static page: server route.
- Page with
Linknavigation only: server route plus navigation runtime. - Small interactive widget: inferred client-interactive component.
- Explicit integration boundary:
.client.tsx. - Whole page is browser-owned: route-level
"use client". - Browser behavior is hidden behind dynamic dispatch: explicit
.client.tsxor"use client".