LiveViewReact logo: a warm phoenix transitioning into React cyan

English · 한국어 · 日本語 · 简体中文

React 19 inside Phoenix LiveView, with LiveView still in charge.

HexDocs · Hex · Getting started · Comparison · Limitations

LiveViewReact mounts normal React roots inside Phoenix LiveView. LiveView continues to own routing, authoritative server state, validation, reconnects, and DOM replacement. React owns only the component tree you explicitly mount. There is no second socket, hidden page-wide root, or SPA runtime.

If you are evaluating fit, read Why, Runtime model, and Boundaries first.

Features

Why

Pure LiveView is still the right default for most Phoenix screens. Use LiveViewReact when one bounded part of the page needs React itself:

If React should own the whole page shell, routing, and remote data lifecycle, use an SPA or Inertia-style architecture instead.

Install

From the Phoenix application root:

mix igniter.install liveview_react

The installer wires PhoenixVite, React, TypeScript, the browser entrypoint, the SSR entrypoint, and a component registry.

Useful variants:

In an umbrella, run the installer from the Phoenix child application, not the umbrella root.

If mix igniter.install is unavailable, install the Igniter archive first with mix archive.install hex igniter_new. Then run mix assets.setup and mix phx.server; the generated demo is available at /liveview-react. See Installation for the complete generated-file and asset workflow.

First component

Create assets/react-components/Counter.tsx:

import { useState } from "react";
import { useLiveViewReact } from "liveview_react";
type CounterProps = {
readonly count: number;
};
export default function Counter({ count }: CounterProps) {
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const { pushEvent } = useLiveViewReact();
async function increment() {
setErrorMessage(null);
try {
await pushEvent("increment", { by: 1 });
} catch {
setErrorMessage("The server could not process the increment");
}
}
return (
<>
<button type="button" onClick={() => void increment()}>
Count: {count}
</button>
{errorMessage && <p role="alert">{errorMessage}</p>}
</>
);
}

Render it from LiveView:

<.react
id="account-counter"
component="Counter"
socket={@socket}
count={@count}
/>

Initialize the server state and handle the event in LiveView:

def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
def handle_event("increment", %{"by" => by}, socket)
when is_integer(by) and by in 1..10 do
socket = update(socket, :count, &(&1 + by))
{:reply, %{count: socket.assigns.count}, socket}
end
def handle_event("increment", _params, socket) do
{:reply, %{error: "increment must be an integer from 1 through 10"}, socket}
end

The default registry names components by their extensionless path under assets/react-components, so assets/react-components/Counter.tsx becomes "Counter".

Runtime model

What LiveView owns:

What React owns:

A normal prop update rerenders the existing root and preserves local React state. Removing the <.react> element, changing its id, or changing its component is a deliberate remount boundary.

Client API

LiveViewReact exports:

Call bridge commands from effects or event handlers, never during render. The low-level commands returned by useLiveViewReact() intentionally throw during SSR and the hydration render pass; the built-in hooks provide their documented post-commit hydration behavior. See Client hooks.

Minimal client entrypoint:

import { Socket } from "phoenix";
import { LiveSocket } from "phoenix_live_view";
import components from "virtual:liveview-react/components";
import { createLiveViewReact } from "liveview_react";
const liveViewReact = createLiveViewReact({ components });
const liveSocket = new LiveSocket("/live", Socket, {
hooks: {
...liveViewReact.hooks,
},
params: { _csrf_token: csrfToken },
});

Minimal SSR entrypoint:

import components from "virtual:liveview-react/components";
import { createLiveViewReactServer } from "liveview_react/server";
export const { render } = createLiveViewReactServer({ components });

Streams, slots, and navigation

Boundaries

These are intentional product constraints, not compatibility gaps:

Requirements

Guides

Development

Project checks:

mix quality
npm run quality
npm run test:e2e

This repository includes a Phoenix example application under liveview_react_examples for SSR, lifecycle, stream, slot, and navigation verification.

Maintainer-grade verification adds mix quality_full, npm run quality:ci, and the hosted Release Please workflow. See Testing and Releasing before merging a release PR.

Credits

LiveViewReact began as a fork of LiveReact by Baptiste Chaleil (Mrdotb). It has since been substantially redesigned and reimplemented as an independent project with its own package identity, public API, runtime, and transport protocol. The original MIT copyright notice remains for inherited portions of the codebase.

The project also draws significant inspiration from LiveVue and LiveSvelte, particularly around LiveView integration, SSR, streams, slots, and developer experience.

License

Copyright (c) 2026 Geonwoo Jeong. Portions copyright (c) 2024 Mrdotb. Released under the MIT License.