This guide describes the supported Elixir component and JavaScript entry points. Internal transport modules and DOM attributes are not application APIs.

HEEx component

Import LiveViewReact in the application's web helpers and call react/1:

<.react
  id="profile-editor"
  component="ProfileEditor"
  socket={@socket}
  user={@user}
  ssr={true}
  diff={true}
/>
AssignContract
idRequired non-empty string; stable and unique in the rendered DOM
componentRequired non-empty registry key; stable while mounted
socketRequired current Phoenix.LiveView.Socket
ssrOptional boolean; defaults to config :liveview_react, ssr: true
diffOptional boolean; defaults to config :liveview_react, enable_props_diff: true
all other assignsEncoded as immutable React props, streams, events, or slots

The outer element is transport-only. class, style, and other ordinary assigns are component props; put outer visual markup inside the React component.

Pass @streams.name directly and use each item's transported __dom_id as its React key. Default HEEx content becomes React children; named slots are written as <:slot name="header"> and become same-name React props. Slot HTML uses a fail-closed inert-markup allowlist and cannot contain links, form controls, resource-bearing tags, event/style/URL attributes, LiveView bindings or hooks, LiveComponents, or nested React roots. See Streams and Slots.

An r-on:save={%Phoenix.LiveView.JS{}} attribute becomes an onSave callback prop. Event names must be lowercase kebab case. Callback names cannot collide with ordinary props. See Events.

Component registry

The registry uses explicit tagged entries. Direct component values and untagged zero-argument functions are rejected:

import Counter from "./Counter";

const components = {
  Counter: { component: Counter },
  Editor: { load: () => import("./Editor") },
} as const;

An eager entry accepts React function and class components plus memo, forwardRef, and lazy exotic components. A registry loader must resolve to { default: Component }.

The installer normally supplies the immutable default export from virtual:liveview-react/components. It recursively registers default-exported .js, .jsx, .ts, and .tsx files below ./react-components, relative to the Vite root. The extensionless relative path is the key. The virtual registry is eager; use an explicit tagged registry when bridge-level code splitting is required.

Browser factory

Create the bridge once and merge its hook map into the application's existing LiveSocket hooks:

import components from "virtual:liveview-react/components";
import { createLiveViewReact } from "liveview_react";

const liveViewReact = createLiveViewReact({
  components,
  strictMode: true,
  wrapRoot({ children, componentName, element }) {
    return (
      <AppProviders root={componentName} element={element}>
        {children}
      </AppProviders>
    );
  },
  onCaughtError(error, info) {
    reportReactError("caught", error, info);
  },
  onRecoverableError(error, info) {
    reportReactError("recoverable", error, info);
  },
  onUncaughtError(error, info) {
    reportReactError("uncaught", error, info);
  },
});

components is required. strictMode defaults to false. wrapRoot receives immutable { children, componentName, element } for each root. The three error callbacks are passed to React 19's client root API and are client-only. Unknown options, accessors, and invalid values fail immediately.

element is null during SSR and the hydration pass. It is the outer hook element for client-only roots and client renders after hydration. Do not derive different visible provider markup from that value during hydration.

The factory returns { hooks: { LiveViewReactHook } }. The main package also exports Link, useLiveViewReact, useLiveEvent, useEventReply, useLiveConnection, useLiveNavigation, useLiveForm, and useLiveUpload, with their public TypeScript types.

Low-level useLiveViewReact() bridge commands are unavailable during SSR and the hydration render pass. Invoke commands only from effects or event handlers; the built-in hooks use their documented post-commit client bridge during hydration. See Client hooks.

Vite plugin

Both import forms are supported and refer to the same plugin factory:

import liveViewReactPlugin from "liveview_react/vite";
// or: import { liveViewReactPlugin } from "liveview_react/vite";

Vite 8 is an optional peer because only applications importing this plugin subpath need Vite at runtime.

liveViewReactPlugin({
  componentDirectory: "./react-components",
  entrypoint: "./js/liveview_react_server.tsx",
  maxBodyBytes: 1_048_576,
  path: "/ssr_render",
});
OptionDefaultPurpose
componentDirectory./react-componentsVirtual-registry source inside the Vite root
entrypoint./js/server.tsVite SSR module exporting render
maxBodyBytes1_048_576Maximum development SSR request body size
path/ssr_renderAbsolute development SSR endpoint path

The directory must stay inside the Vite root and cannot traverse symlinks. The SSR endpoint accepts only POST JSON with an optional single charset parameter. Unknown plugin options and malformed requests fail explicitly.

Server factory

The server entry uses the same registry:

import components from "virtual:liveview-react/components";
import { createLiveViewReactServer } from "liveview_react/server";
import type { ServerRenderRequest } from "liveview_react/server";

const server = createLiveViewReactServer({ components });

export function render(request: ServerRenderRequest): Promise<string> {
  return server.render(request);
}

createLiveViewReactServer accepts components, strictMode, and wrapRoot. Client root error callbacks are rejected on the server. Applications normally receive ServerRenderRequest from the BEAM adapter rather than constructing it. That request is the exact transport-v2 initial frame; version, component, identifierPrefix, props, streams, events, and slots are all required, and unknown fields are rejected.

Public error classes

The main liveview_react entry exports four operational error classes:

  • LiveEventReplyCancelledError: a reply was superseded, cancelled, or its component unmounted.
  • LiveEventReplyTimeoutError: useEventReply exceeded its configured timeout.
  • LiveFormSubmitCancelledError: submission could not start or was cancelled by reset, disconnect, unmount, or form replacement.
  • LiveFormSubmitInvalidError: native browser constraint validation blocked submission.

Invalid JavaScript configuration or wire data raises TypeError; missing registry entries and invalid mounted identity raise Error. On the BEAM side, LiveViewReact.SSR.NotConfigured reports absent SSR infrastructure, while LiveViewReact.SSR.RenderError reports invalid SSR requests, renderer failures, or invalid renderer responses. Invalid component assigns raise ArgumentError. Do not parse error message text as an API.