foreign() — mount a React component unchanged#

import { foreign, isForeignDef } from 'janux/interop';

foreign(component: unknown, options?: ForeignOptions): ForeignTag wraps a component from a foreign runtime (React today) so it can be used in a Janux view. The result is TSX-callable like a Janux component, but it mounts a real embedded root — full behavioral fidelity for libraries that depend on their own scheduler, portals and synthetic events. Guide: Foreign-UI interop.

import { foreign } from 'janux/interop';
import { ReactFlow } from '@xyflow/react';

const Flow = foreign(ReactFlow, {
  props: (own) => ({ nodes: own.state.nodes }),
  on: { onNodeDrag: 'moveNode' },
  hydrate: 'visible',
});

ForeignOptions#

Option Type Default What it does
name string the component's displayName or name, else 'foreign' Island name used in ids and markup
props (own) => Record<string, unknown> Maps the JSX call-site props (typically the shell's state) to the foreign component's props. Tracked: signal reads re-render only the foreign root
on Record<string, string> Maps a foreign callback prop to an intent name on the enclosing island. The callback's first argument becomes the intent input
hydrate 'load' | 'idle' | 'visible' | 'only' 'load' When the client root mounts

Hydrate directives#

Value Behavior
load SSR, then mount as soon as the runtime boots
idle SSR, then mount in an idle callback
visible SSR, then mount when the host scrolls into view (IntersectionObserver)
only Skip SSR — client-render only. For components that can't render on the server

With react/react-dom installed, everything except only server-renders inside its host so the user sees paint before JS. The client then does a deterministic render-replace over that markup rather than a mismatch-prone hydration. SSR here is fail-soft: if the runtime isn't installed, or your props mapper or the React render throws, the host ships empty and the client mounts it anyway — so a blank host in the HTML means "look at the mapper", not "interop is broken".

isForeignDef(type)#

isForeignDef(type: unknown): type is ForeignDef — the type guard the renderer uses to tell a foreign tag from a Janux component. Import it when you write code that walks a view tree (a custom renderer, a devtool, a test helper) and must branch on the two kinds:

if (isForeignDef(node.$t)) {
  // an opaque foreign leaf: render its host, don't walk into it
}

ForeignDef is { kind: 'foreign', name, component, options } and is frozen, so a def can be shared safely across requests.

Agents see nothing by default#

A foreign island projects a view only — no resource, no tools. That's deliberate: the framework can't know the semantics of someone else's component. Wrap it in a bifacial shell whose state it renders and whose intents drive it, and the copilot gets full control without DOM inference. The interop guide shows the wrap-once pattern, and examples/interop-react is a working app.

Related: Foreign-UI interop · Ownership · Examples