Skip to content

gsmlg-dev/phoenix-react

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Phoenix.React

CI Hex.pm Hexdocs.pm Hex.pm Hex.pm

Run a react render server to render react component in Phoenix html.

Features

  • Render to static markup
  • Render to string
  • Render to readable stream
  • Hydrate at client side
  • Connect to Live View render

See the docs for more information.

Install this package

Add deps in mix.exs

{:phoenix_react_server, "~> 0.5"},

Configuration

Set config, runtime, react components, etc.

import Config

config :phoenix_react_server, Phoenix.React,
  # react runtime, default to `bun`
  runtime: Phoenix.React.Runtime.Bun,
  # react component base path
  component_base: Path.expand("../assets/component", __DIR__),
  # cache ttl, default to 60 seconds
  cache_ttl: 60

Supported runtime

  • Phoenix.React.Runtime.Bun
  • Phoenix.React.Runtime.Deno

Add Render Server in your application 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.React,
      ReactDemoWeb.Endpoint
    ]

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

Write Phoenix Component use react_component

defmodule ReactDemoWeb.ReactComponents do
  use Phoenix.Component

  import Phoenix.React.Helper

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

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

Import in html helpers in react_demo_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

render_to_static_markup

Then you can use react server rendered component in Phoenix Component

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

render_to_string

<div class="card w-full">
  <div class="card-body">
    <h3 class="card-title">
      This <code class="text-primary">Table</code> is rendered with <code class="text-secondary">react-dom/server</code>
    </h3>
    <!-- Notice: Remove white space in the react render node or it will break hydrate -->
    <!-- Add `phx-no-format` to avoid mix format change the code here -->
    <div
      id="system_usage_container"
      class="w-full h-full"
      phx-no-format
    ><.react_system_stats
      data={@data}
    /></div>
  </div>
</div>
  • Then hydrate on the client.
document.addEventListener('DOMContentLoaded', function() {
  const store = new Store();
  const domContainer = document.querySelector('#system_usage_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} />;
    }

    let root;

    channel.on("joined", (data) => {
      // console.log("Reset stats: ", data)
      store.reset(data.data)

      requestAnimationFrame(() => {
        hydrateRoot(domContainer, <Usage />);
      });
    });

    channel.on("stats", (data) => {
      // console.log("Stats: ", data)
      store.unshift(data)
    });

  }
});

render_to_readable_stream

<div
  id="react-live-form"
  class="w-full h-full"
  phx-update="ignore"
  phx-hook="LiveFormHook"
><.react_live_form
  data={@form_data}
/></div>
  • Work with LiveView
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);
      });
    },
  }
}

Run in release mode

Bundle components with server.js to one file.

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

Config runtime to Phoenix.React.Runtime.Bun in runtime.exs

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

Hydrate at client side with CDN

Hydrate react component at client side when CDN.

<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 { Component } from 'app';

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

DEMO

Path ./react_demo

About

React Render for Phoenix Framework

Resources

License

Stars

Watchers

Forks

Packages

No packages published