Phoenix.ReactServer (Phoenix.ReactServer NG v0.8.5)

Copy Markdown View Source

Phoenix.ReactServer provides high-performance server-side rendering (SSR) for React components within Phoenix applications. It supports both Bun and Deno JavaScript runtimes with intelligent caching and hot reloading capabilities.

Features

  • [x] Server-side rendering with multiple output formats:
    • Static markup generation (SEO-friendly)
    • String rendering with client hydration
    • Readable stream rendering (LiveView integration)
  • [x] Dual runtime support (Bun and Deno)
  • [x] Intelligent caching with TTL management
  • [x] Hot reloading in development mode
  • [x] LiveView integration with streaming support
  • [x] Client-side hydration capabilities
  • [x] Bundle size optimization (67% smaller than previous versions)

See the docs for more information.

Install this package

Add deps in mix.exs

{:phoenix_react_ng, "~> 0.5"},

Installation

Add to your dependencies in mix.exs:

{:phoenix_react_ng, "~> 0.5"},

Configuration

Configure the runtime, component paths, and caching options:

import Config

config :phoenix_react_ng, Phoenix.ReactServer,
  # Runtime: Bun (default) or Deno
  runtime: Runtime.Bun,
  # React component base path
  component_base: Path.expand("../assets/component", __DIR__),
  # Cache TTL in seconds (default: 60, set to 0 to disable)
  cache_ttl: 60

Supported Runtimes

  • Bun Runtime (Runtime.Bun): Fast startup, excellent performance
  • Deno Runtime (Runtime.Deno): Secure runtime with npm package support

Supervisor Configuration

Add to your application's supervisor tree:

def start(_type, _args) do
  children = [
    ReactDemoWeb.Telemetry,
    {DNSCluster, query: Application.get_env(:react_demo, :dns_cluster_query) || :ignore},
    {Phoenix.PubSub, name: ReactDemo.PubSub},
    # React render service
    Phoenix.ReactServer,
    ReactDemoWeb.Endpoint
  ]

  opts = [strategy: :one_for_one, name: ReactDemo.Supervisor]
  Supervisor.start_link(children, opts)
end

Usage Examples

Phoenix Component

Create Phoenix components that render React components:

defmodule ReactDemoWeb.ReactComponents do
  use Phoenix.Component
  import Phoenix.ReactServer.Helper

  def react_markdown(assigns) do
    {static, props} = Map.pop(assigns, :static, true)

    react_component(%{
      component: "markdown",
      props: props,
      static: static
    })
  end
end

Import your React components in lib/your_app_web.ex:

defp html_helpers do
  quote do
    # Translation
    use Gettext, backend: ReactDemoWeb.Gettext
    # HTML escaping functionality
    import Phoenix.HTML
    # Core UI components
    import ReactDemoWeb.CoreComponents
    import ReactDemoWeb.ReactComponents
  end
end

Rendering Methods

Static Markup (SEO-friendly)

Use static: true for SEO-friendly content that doesn't need client-side interaction:

<div class="card">
  <div class="card-body">
    <div class="card-title">Hello There</div>
    <.react_markdown data={@data} static={true} />
  </div>
</div>

String Rendering with Hydration

Use static: false (default) for components that need client-side interaction:

<div class="card w-full">
  <div class="card-body">
    <h3 class="card-title">
      This <code class="text-primary">Component</code> is rendered with server-side rendering
    </h3>
    <!-- Note: No whitespace between container and component for proper hydration -->
    <div class="w-full h-full" id="interactive-container"><.react_interactive_component data={@data} /></div>
  </div>
</div>

Then hydrate on the client:

import { hydrateRoot } from 'react-dom/client';

document.addEventListener('DOMContentLoaded', function() {
  const store = new Store();
  const domContainer = document.querySelector('#interactive-container');

  if (domContainer) {
    let channel = socket.channel("system_usage:lobby", {});

    channel.join()
      .receive("ok", resp => { console.log("Joined successfully", resp) })
      .receive("error", resp => { console.log("Unable to join", resp) });

    function Usage(props) {
      const data = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot);
      return <SystemUsage data={data} />;
    }

    channel.on("joined", (data) => {
      store.reset(data.data);
      requestAnimationFrame(() => {
        hydrateRoot(domContainer, <Usage />);
      });
    });

    channel.on("stats", (data) => {
      store.unshift(data);
    });
  }
});

Streaming with LiveView

Use streaming rendering for dynamic LiveView components:

<div
  id="react-live-form"
  class="w-full h-full"
  phx-update="ignore"
  phx-hook="LiveFormHook"
><.react_live_form data={@form_data} /></div>

Create LiveView hooks for streaming components:

const hooks = {
  LiveFormHook: {
    mounted() {
      const formState = new FormState();

      formState.setData = (data) => {
        this.pushEvent("form:input", data);
      };

      function LiveViewForm(props) {
        const data = useSyncExternalStore(
          formState.subscribe,
          formState.getSnapshot,
          formState.getServerSnapshot
        );
        return <LiveForm data={data} setData={formState.setData} />;
      }

      this.pushEvent("form:init", {}, (data, ref) => {
        formState.reset(data);
        this.reactRoot = hydrateRoot(this.el, <LiveViewForm />);
      });

      this.handleEvent("form:update", (data) => {
        formState.assign(data);
      });
    },
  }
}

Production Deployment

Bundle components and server code for production releases.

Production Bundling

For Bun Runtime:

mix phx.react.bun.bundle --component-base=assets/component --output=priv/react/server.js

For Deno Runtime:

mix phx.react.deno.bundle --component-base=assets/component --output=priv/react/server.js

Production Configuration

Configure in runtime.exs:

Bun Runtime:

config :phoenix_react_ng, Runtime.Bun,
  cmd: System.find_executable("bun"),
  server_js: Path.expand("../priv/react/server.js", __DIR__),
  port: 12666,
  env: :prod

Deno Runtime:

config :phoenix_react_ng, Runtime.Deno,
  cmd: System.find_executable("deno"),
  server_js: Path.expand("../priv/react/server.js", __DIR__),
  port: 12667,
  env: :prod

Client-Side Hydration with CDN

Hydrate React components on the client side using CDN modules:

<script type="importmap">
  {
    "imports": {
      "react-dom": "https://esm.run/react-dom@19",
      "app": "https://my.web.site/app.js"
    }
  }
</script>
<script type="module">
  import { hydrateRoot } from 'react-dom/client';
  import { App } from 'app';

  hydrateRoot(
    document.getElementById('app-wrapper'),
    <App />
  );
</script>

React Component Structure

React components should be placed in your configured component_base directory and export a default Component function:

// assets/component/my_component.js
import React from 'react';

export function Component({ title, children }) {
  return (
    <div className="my-component">
      <h1>{title}</h1>
      {children}
    </div>
  );
}

Performance Considerations

  • Bundle Size: Components are optimized with 67% size reduction
  • Caching: Intelligent caching with configurable TTL
  • Hot Reloading: Automatic in development mode
  • Streaming: Support for LiveView integration with streaming

Summary

Types

React component file name without extension.

Configuration options for Phoenix.ReactServer.

React component props.

Rendering method for React components.

Render result containing the HTML output.

Functions

Returns a specification to start this module under a supervisor.

Find the process ID of the React server.

Initializes the Phoenix.ReactServer supervisor with child processes.

Render a React component to a readable stream.

Render a React component to static HTML markup.

Render a React component to an HTML string with hydration support.

Starts the Phoenix.ReactServer supervisor.

Stop the React runtime process.

Types

component()

@type component() :: String.t()

React component file name without extension.

The component file must export a Component function that accepts props and returns a React element.

Example

# For file "assets/component/chart.jsx"
component = "chart"

config()

@type config() :: %{
  optional(:runtime) => module(),
  optional(:component_base) => Path.t(),
  optional(:render_timeout) => timeout(),
  optional(:cache_ttl) => non_neg_integer()
}

Configuration options for Phoenix.ReactServer.

props()

@type props() :: map()

React component props.

Must be a JSON-serializable map that can be passed to the React component. All keys and values must be serializable to JSON.

Example

props = %{
  "data" => [1, 2, 3],
  "title" => "My Chart",
  "options" => %{ "color" => "blue" }
}

render_method()

@type render_method() ::
  :render_to_static_markup | :render_to_string | :render_to_readable_stream

Rendering method for React components.

  • :render_to_static_markup - Renders to static HTML (no React data attributes)
  • :render_to_string - Renders to HTML with React data attributes for hydration
  • :render_to_readable_stream - Renders to a readable stream for large components

render_result()

@type render_result() :: {:ok, String.t()} | {:error, term()}

Render result containing the HTML output.

The HTML string contains the rendered React component and can be directly embedded in Phoenix templates.

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

find_server_pid()

@spec find_server_pid() :: pid() | nil

Find the process ID of the React server.

Returns

  • pid() - The server process ID
  • nil - Server not found

Used internally by render functions to locate the server process.

init(init_arg)

@spec init(term()) ::
  {:ok, {:supervisor.sup_flags(), [:supervisor.child_spec()]}} | :ignore

Initializes the Phoenix.ReactServer supervisor with child processes.

Children

  • Cache - ETS-based caching for rendered components
  • Runtime - Dynamic supervisor for JavaScript runtimes
  • Server - GenServer handling rendering requests

Returns

  • {:ok, children} - Supervisor initialized successfully

render_to_readable_stream(component, props \\ %{})

@spec render_to_readable_stream(component(), props()) :: render_result()

Render a React component to a readable stream.

Uses renderToReadableStream from react-dom/server for optimal performance with large components or streaming scenarios.

Parameters

  • component - The component name (file without extension)
  • props - JSON-serializable map of component props (default: %{})

Returns

  • {:ok, html} - Successfully rendered HTML string
  • {:error, reason} - Rendering failed with error reason

Example

iex> Phoenix.ReactServer.render_to_readable_stream("chart", %{"data" => [1, 2, 3]})
{:ok, "<div>...</div>"}

render_to_static_markup(component, props)

@spec render_to_static_markup(component(), props()) :: render_result()

Render a React component to static HTML markup.

Uses renderToStaticMarkup from react-dom/server to generate static HTML without React data attributes. Ideal for SEO content that doesn't need client-side interactivity.

Parameters

  • component - The component name (file without extension)
  • props - JSON-serializable map of component props

Returns

  • {:ok, html} - Successfully rendered HTML string
  • {:error, reason} - Rendering failed with error reason

Example

iex> Phoenix.ReactServer.render_to_static_markup("markdown", %{"content" => "# Hello"})
{:ok, "<h1>Hello</h1>"}

render_to_string(component, props \\ %{})

@spec render_to_string(component(), props()) :: render_result()

Render a React component to an HTML string with hydration support.

Uses renderToString from react-dom/server to generate HTML that includes React data attributes for client-side hydration.

Parameters

  • component - The component name (file without extension)
  • props - JSON-serializable map of component props (default: %{})

Returns

  • {:ok, html} - Successfully rendered HTML string
  • {:error, reason} - Rendering failed with error reason

Example

iex> Phoenix.ReactServer.render_to_string("chart", %{"data" => [1, 2, 3]})
{:ok, "<div data-reactroot="">...</div>"}

start_link(init_arg)

@spec start_link(term()) :: GenServer.on_start()

Starts the Phoenix.ReactServer supervisor.

Parameters

  • init_arg - Initialization arguments (typically [])

Returns

  • {:ok, pid} - Supervisor started successfully
  • {:error, reason} - Failed to start supervisor

Example

iex> Phoenix.ReactServer.start_link([])
{:ok, #PID<0.123.0>}

stop_runtime()

@spec stop_runtime() :: :ok | {:error, term()}

Stop the React runtime process.

Useful for development when you need to restart the runtime after configuration changes.

Returns

  • :ok - Runtime stopped successfully
  • {:error, reason} - Failed to stop runtime