Guides
React Compatibility
Mreact's primary path for new applications is the compiled App Router model with native cell(), computed(), and effect() primitives. The workspace also includes @reckona/mreact-compat for React-style authoring and runtime behavior. Use the compatibility path when migrating an existing React codebase, evaluating a third-party React library, or integrating code that depends on React-style JSX runtimes, hooks, roots, scheduling, hydration, Suspense, or server rendering APIs.
Most applications should import from @reckona/mreact. React-style application code should import compatibility exports from that entrypoint:
import { createElement, useState } from "@reckona/mreact";Importing @reckona/mreact-compat directly is mainly for tests, framework integration, compatibility experiments, and package-level debugging:
import ReactCompat, { createElement, useState } from "@reckona/mreact-compat";
import { jsx } from "@reckona/mreact-compat/jsx-runtime";Package location and execution mode are separate decisions. A component imported from @reckona/mreact can still become a client boundary when its route path needs browser behavior, and a compatibility component can remain server-renderable when its compiled shape is safe. Use the Basics guide for the native starting path and the Server and Client Model for boundary ownership.
| Situation | Authoring choice | Boundary guidance |
| --- | --- | --- |
| New application code | Native App Router with cell, computed, and effect | Let inference keep the route server-rendered around the interactive path. |
| React migration or ecosystem package | @reckona/mreact or @reckona/mreact-compat | Keep the compatibility surface focused and verify the library behavior you use. |
| Imperative DOM integration | @reckona/mreact-reactive-dom | Make the root or cleanup scope the owner of every binding and listener. |
For example, a native counter uses count.get() as its tracked read:
import { cell } from "@reckona/mreact-reactive-core";
export function NativeCounter() {
const count = cell(0);
return <button onClick={() => count.set((value) => value + 1)}>{count.get()}</button>;
}The compatibility equivalent uses the React-style hook contract:
import { useState } from "@reckona/mreact";
export function CompatCounter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount((value) => value + 1)}>{count}</button>;
}Optimization or compatibility fallback can change how much code runs and ships, but it should not change the observable counter behavior. Keep low-level DOM work under an explicit createRoot() or createCleanupScope() owner so listeners, effects, subscriptions, and pending work are disposed with the integration.
For compiled compat code, static calls to useState, useReducer, useRef, useMemo, useCallback, useEffect, useLayoutEffect, useId, useContext, or useInsertionEffect through a default import from @reckona/mreact can be emitted as named imports. This lets bundlers remove unused compatibility exports while preserving the public default object. Dynamic property access, passing or modifying that object, unsupported members, and uncertain references keep the original import. App Router shares this import proof with the compiler before applying its independent SSR safety checks, so a default binding used exclusively for supported direct hook calls can participate in SSR like the equivalent named imports. The mutable default object from @reckona/mreact-compat remains outside this optimization.
Type-only references to that default binding, such as type StateHook = typeof React.useState, do not block hook import normalization or eligible App Router SSR. The compiler retains required type references through import type. Runtime expressions inside as, satisfies, non-null assertions, and other TypeScript constructs are still checked: passing React as unknown to another function keeps the original runtime import.
App Router compat SSR
The App Router server-renders eligible .compat.tsx boundaries and hydrates their existing DOM. A counter is visible before JavaScript loads; hydration preserves its button and focus. When the server and first client props agree, it also preserves edits made before hydration to text inputs, checkboxes, textareas, and selects, including controlled controls. File inputs and recovery that replaces a mismatched DOM range are outside this preservation contract. Each boundary owns a comment-delimited range, so sibling counters keep independent state without extra wrapper elements. Route navigation disposes their effects, refs, and event resources.
Eligibility is conservative and checked across runtime imports and re-exports without evaluating application modules. The supported subset includes synchronous components, deterministic initial state, dynamic attributes and styles, form controls, useId(), and Context trees. Named compatibility exports from @reckona/mreact, @reckona/mreact-compat, or react, relative pure helpers, and nested .compat components can participate. Nested compat components render within their owning boundary; they do not create another independent root. Identifiers use a per-boundary prefix shared by SSR and hydration. This applies to development rendering, production string and streaming rendering, and the Lambda and Cloudflare render adapters.
// Counter.compat.tsx
import { useState } from "@reckona/mreact-compat";
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(value => value + 1)}>Count: {count}</button>;
}A boundary stays client-only when its dependency graph cannot be proven eligible. This includes browser globals, unknown packages, side-effect imports, arbitrary module-level calls or mutable objects, nondeterministic operations, dynamic imports, dependency cycles, and ordinary non-compat JSX helpers. A custom source transform also retains the client-only path. The MR_COMPAT_SSR_CLIENT_ONLY diagnostic identifies the dependency and rejected operation. An explicit "use client" directive opts out of compat SSR; .client boundaries also retain their existing behavior.
Module-level Context creation is supported when a named createContext import directly initializes a const with a literal string, finite number, boolean, null, or unshadowed undefined default. Import aliases, TypeScript type arguments, and relative named or default Context exports and re-exports are supported. Context identities may be used through useContext(Theme), <Theme.Provider>, <Theme.Consumer>, or <Theme> inside the owning boundary. Mutable object defaults, factory wrappers, Context aliases or internal member access, namespace imports of Context exports, and passing the Context object to arbitrary helpers keep the boundary client-only.
For graphs containing Context, ReactElement type introspection and JSON reflection are also excluded across all runtime dependencies: they can expose the Context identity indirectly. This conservative check also excludes unrelated type property access or destructuring and JSON operations in the same graph. Ordinary component nesting and passing rendered children remain supported.
// theme.ts
import { createContext } from "@reckona/mreact";
export const Theme = createContext<string | null>(null);
// ThemePanel.compat.tsx
import { useContext, useState } from "@reckona/mreact";
import { Theme } from "./theme";
function Label() {
return <output>{useContext(Theme)}</output>;
}
export function ThemePanel() {
const [theme, setTheme] = useState("light");
return <Theme.Provider value={theme}><Label /><button onClick={() => setTheme("dark")}>Change theme</button></Theme.Provider>;
}SSR makes this Context tree available in the initial HTML and hydration reuses its DOM. The compatibility JavaScript is still needed to attach events and update Context; SSR eligibility does not by itself reduce its download size or make controls interactive before hydration. Compare initial HTML, initial JavaScript, first-interaction imports, and the time until an update succeeds when evaluating a library.
Compat boundary string return values are escaped as text, including strings that resemble HTML. The public @reckona/mreact-dom/server APIs also escape string roots as text across string, static-markup, and streaming rendering. When calling the low-level renderToString() helper from @reckona/mreact-compat/server directly for a ReactNode component, pass { stringResult: "text" }; its default preserves legacy compiler-generated HTML strings. Both React-compatible and native SSR reject malformed dynamic tag names before producing HTML. If external input selects a tag, use an application-specific allowlist as well: a syntactically valid tag such as script can still be unsuitable for your application.
Boundary props use the same JSON-normalized values on the server and client. Nonserializable props, including cyclic values and BigInt, keep the existing client-only fallback. Server-rendered JSX children retain the existing slot path and do not enable compat SSR. A Provider outside a boundary does not transfer context across independent roots; pass serializable values explicitly. These restrictions bound the supported SSR subset; they do not make arbitrary browser-dependent components safe to execute on the server.
Malformed props or missing range markers leave that boundary untouched while other valid boundaries continue hydrating. Recoverable mismatches are limited to the affected range. Deployment checks cover the generated Cloudflare route registry and Lambda render adapter; hosted infrastructure and a separately installed Lambda deployment package require their usual deployment smoke tests.
Vite configuration
You usually do not need a separate Vite alias just to use Mreact's app router. The mreactRouter() plugin wires the framework runtime, and most route code should import from @reckona/mreact.
When you evaluate React ecosystem libraries, the Vite configuration may still matter for normal Vite reasons. The react-libraries example pre-bundles browser-side React ecosystem packages with optimizeDeps.include so the dev server does not discover them after the page is already interactive and trigger a full reload. It also excludes server-only packages such as better-sqlite3 from dependency optimization and allows that package through the router import policy for server modules.
// vite.config.ts
import { defineConfig } from "vite";
import { mreactRouter } from "@reckona/mreact-router/vite";
export default defineConfig({
optimizeDeps: {
exclude: ["better-sqlite3"],
include: [
"recharts",
"lexical",
"@lexical/react/LexicalComposer",
"@conform-to/react",
"@conform-to/zod",
"zod",
"@radix-ui/react-dialog",
],
},
plugins: [
mreactRouter({
routesDir: "app",
importPolicy: {
allowedPackages: ["better-sqlite3"],
},
}),
],
});Treat optimizeDeps.include as a dev-experience tool, not a compatibility switch. If a library imports browser globals during server render, let inference handle visible browser behavior or isolate browser-only code in a .compat.tsx component.
What it covers
@reckona/mreact-compat implements React compatibility for element creation, fragments, refs, hooks, context, roots, scheduling, reconciliation, hydration, Suspense, server rendering, resource hints, JSX runtimes, and selected Flight integration surfaces.
The compatibility gate tracks React 19.2.6 public export coverage for React, React DOM, React DOM client, and React DOM server. It is a compatibility runtime, not a byte-for-byte React reconciler, so use tests for the specific library behavior you depend on.
Compiler optimization
The compatibility runtime is not the only execution path for React-shaped code. When React-compatible JSX is compiled by Mreact, the compiler can lower proven-safe state text, prop bindings, and event bindings into reactive DOM blocks. Effect-sensitive, ref-sensitive, or otherwise unsupported shapes keep the normal compatibility reconciler path, so the optimization preserves observable behavior instead of acting as a benchmark-only shortcut.
App Router output
Static React-compatible createElement() trees can still flow through the App Router server string pipeline, so a server-renderable compat subtree does not need to pay the same runtime cost as a fully dynamic client integration. When a route uses compat client references, the router shares compat vendor chunks only across route artifacts that actually need them, keeping server-only and non-compat routes out of that client payload.
Use this as a performance model, not a compatibility switch: start from server-renderable route code, isolate browser-only libraries behind narrow client-interactive boundaries, and check the route's client bundle size on the Benchmarks page or in your own production build.
Known differences
useId() returns opaque underscore-delimited ids such as _R_0_ and _r_0_ instead of React's colon-delimited internal shape. Mreact keeps SSR and hydration ids stable through its hydrated-id map, but application code and libraries should treat the returned string as opaque and must not parse or compare it against React's exact internal format.
Server rendering normalizes standard HTML attribute casing, including readOnly. If a third-party library depends on byte-for-byte React markup rather than browser-observable DOM behavior, keep a focused compatibility test around that integration.
Library evaluation workflow
Start with examples/react-libraries on GitHub when you want to evaluate ecosystem packages. That example exercises libraries such as Recharts, Lexical, Conform, Zod, and Radix UI through a real Mreact app.
pnpm --filter @reckona/example-react-libraries dev
pnpm --filter @reckona/example-react-libraries build
pnpm --filter @reckona/example-react-libraries test:e2eIf a package works in the compatibility example but fails in an App Router route, check whether inference can see the browser behavior, whether an explicit client boundary is needed, whether the package imports browser-only modules during server render, and whether it expects React DOM behavior that is not part of the tested compatibility surface yet.
The repository also includes focused compatibility labs for ecosystem packages that need screenshot and DOM-summary comparison against React. Run pnpm compat-lab:radix when changing Radix-facing event or portal behavior, pnpm compat-lab:recharts when changing chart, SVG, animation, or tooltip behavior, pnpm compat-lab:react-flow when changing React Flow or xyflow-facing layout, store, handle, or pointer behavior, and pnpm compat-lab:ui-primitives when changing primitive-library behavior for Ark UI, Base UI, Headless UI, React Aria, React Spectrum, or Zag JS.
Focused conformance checks
Run the focused compatibility suite when changing the runtime or validating a React-facing behavior:
pnpm test:react-conformance
pnpm exec vitest run packages/react-compat/test/react-official-conformance.test.ts packages/react-compat/test/react-official-suite-gate.test.tsAPI reference: @reckona/mreact-compat, hooks, JSX runtime, scheduler, and server.