Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Fix docs and demo. #3

Merged
merged 4 commits into from
Feb 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 154 additions & 1 deletion lib/phoenix/react.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,151 @@
defmodule Phoenix.React do
@moduledoc """
Phoenix.React

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

**Features**

- [x] Render to static markup
- [x] Render to html
- [x] Hydrate at client side

See the [docs](https://hexdocs.pm/phoenix_react_server/) for more information.

## Install this package

Add deps in `mix.exs`

```elixir
{:phoenix_react_server, "~> 0.2.0"},
```

## Configuration

Set config, runtime, react components, etc.

```elixir
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`

- [x] `Phoenix.React.Runtime.Bun`
- [ ] `Phoenix.React.Runtime.Deno`
- [ ] `Phoenix.React.Runtime.Node`
- [ ] `Phoenix.React.Runtime.Lambda`

Add Render Server in your application Supervisor tree.

```elixir
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`

```elixir
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`

```elixir
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
```

## Use in otp release

Transcompile react component in release.

```shell
bun build --outdir=priv/react/component ./assets/component/**
```

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

```elixir
config :phoenix_react_server, Phoenix.React,
# Change `component_base` to `priv/react/component`
component_base: :code.priv(:react_demo, "react/component")

config :phoenix_react_server, Phoenix.React.Runtime.Bun,
# include `react-dom/server` and `jsx-runtime`
cd: "/path/to/dir/include/node_modules/and/bunfig.toml",
cmd: "/path/to/bun",
env: :prod
```

## Hydrate at client side

Hydrate react component at client side.

```html
<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>
```


"""
use Supervisor

Expand All @@ -18,7 +163,15 @@ defmodule Phoenix.React do
Supervisor.init(children, strategy: :one_for_one)
end

@typedoc """
React component file name
Must export a `Component` function
"""
@type component :: binary()
@typedoc """
React component props
Must be a json serializable map
"""
@type props :: map()

@doc """
Expand Down
26 changes: 25 additions & 1 deletion lib/phoenix/react/cache.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
defmodule Phoenix.React.Cache do
@moduledoc """
A simple ETS based cache for render react component
Cache for React Component rendering

Cache key is a tuple of component, props and static flag

Remove expired cache every 60 seconds
"""
use GenServer

Expand All @@ -13,12 +17,32 @@ defmodule Phoenix.React.Cache do
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
end

@impl true
@spec init(any()) :: {:ok, %{}}
def init(_) do
state = %{}
ensure_started()
schedule_work()
{:ok, state}
end

@impl true
def handle_info(:gc, state) do
ts = :os.system_time(:seconds)
fun = :ets.fun2ms(fn {key, _, expiration} when ts > expiration -> key end)

:ets.lookup(@ets_table_name, fun)
|> Enum.each(&:ets.delete(@ets_table_name, &1))

schedule_work()
{:noreply, state}
end

defp schedule_work do
# Every 60 seconds
Process.send_after(self(), :gc, 60_000)
end

@doc """
Get a cached value
"""
Expand Down
1 change: 0 additions & 1 deletion lib/phoenix/react/helper.ex
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ defmodule Phoenix.React.Helper do
required: true,
doc: """
react component name
```
"""
)

Expand Down
15 changes: 12 additions & 3 deletions lib/phoenix/react/runtime.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule Phoenix.React.Runtime do
Phoenix.React.Runtime behaviour

"""

defstruct [:component_base, :port, render_timeout: 300_000]

@type t :: %__MODULE__{
Expand All @@ -19,18 +20,26 @@ defmodule Phoenix.React.Runtime do

@callback start([{:component_base, path()}, {:render_timeout, integer()}]) :: port()

# @callback config() :: [{:cmd, path()}, {:server_js, path()}, {:port, integer()}, {:env, atom()}]
@callback config() :: list()

# @callback handle_call({:render_to_string, component(), map()}, GenServer.from(), t()) :: {:reply, {:ok, html()}, t()} | {:reply, {:error, term()}, t()}
@callback render_to_string(component(), map(), GenServer.from(), t()) ::
{:reply, {:ok, html()}, t()} | {:reply, {:error, term()}, t()}

# @callback handle_call({:render_to_static_markup, component(), map()}, GenServer.from(), t()) :: {:reply, {:ok, html()}, t()} | {:reply, {:error, term()}, t()}
@callback render_to_static_markup(component(), map(), GenServer.from(), t()) ::
{:reply, {:ok, html()}, t()} | {:reply, {:error, term()}, t()}

defmacro __using__(_) do
quote do
@behaviour Phoenix.React.Runtime
alias Phoenix.React.Runtime

use GenServer

@impl true
def handle_call({method, component, props}, from, state)
when method in [:render_to_string, :render_to_static_markup] do
apply(__MODULE__, method, [component, props, from, state])
end
end
end
end
12 changes: 10 additions & 2 deletions lib/phoenix/react/runtime/bun.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ defmodule Phoenix.React.Runtime.Bun do
{:noreply, %Runtime{state | port: port}}
end

@impl true
def config() do
cfg = Application.get_env(:phoenix_react_server, Phoenix.React.Runtime.Bun, [])
cmd = cfg[:cmd] || System.find_executable("bun")
Expand All @@ -66,6 +67,13 @@ defmodule Phoenix.React.Runtime.Bun do
args = ["--port", Integer.to_string(config()[:port]), config()[:server_js]]
bun_env = if(config()[:env] == :dev, do: "development", else: "production")

args =
if config()[:env] == :dev do
["--watch" | args]
else
args
end

env = [
{~c"BUN_ENV", ~c"#{bun_env}"},
{~c"COMPONENT_BASE", ~c"#{component_base}"}
Expand Down Expand Up @@ -151,7 +159,7 @@ defmodule Phoenix.React.Runtime.Bun do
end

@impl true
def handle_call({:render_to_string, component, props}, _from, state) do
def render_to_string(component, props, _from, state) do
server_port = config()[:port]

reply =
Expand All @@ -167,7 +175,7 @@ defmodule Phoenix.React.Runtime.Bun do
end

@impl true
def handle_call({:render_to_static_markup, component, props}, _from, state) do
def render_to_static_markup(component, props, _from, state) do
server_port = config()[:port]

reply =
Expand Down
16 changes: 16 additions & 0 deletions lib/phoenix/react/server.ex
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
defmodule Phoenix.React.Server do
@moduledoc """
The React Render Server

Start runtime server set in `Application.get_env(:phoenix_react_server, Phoenix.React)`

"""
require Logger

alias Phoenix.React.Cache

use GenServer

@type second() :: integer()
@type millisecond() :: integer()

@doc """
Return the configuration of the React Render Server from `Application.get_env(:phoenix_react_server, Phoenix.React)`
"""
@spec config() :: [
{:cache_ttl, second()}
| {:component_base, binary()}
| {:render_timeout, millisecond()}
| {:runtime, module()},
...
]
def config() do
config = Application.get_env(:phoenix_react_server, Phoenix.React)

Expand Down
Loading