defmodule LiveReact do @moduledoc """ See READ.md for installation instructions and examples. """ use Phoenix.Component import Phoenix.HTML alias Phoenix.LiveView alias LiveReact.SSR alias LiveReact.Slots require Logger @ssr_default Application.compile_env(:live_react, :ssr, true) @doc """ Render a React component. """ def react(assigns) do init = assigns.__changed__ == nil dead = assigns[:socket] == nil or not LiveView.connected?(assigns[:socket]) render_ssr? = init and dead and Map.get(assigns, :ssr, @ssr_default) # we manually compute __changed__ for the computed props and slots so it's not sent without reason {props, props_changed?} = extract(assigns, :props) {slots, slots_changed?} = extract(assigns, :slots) component_name = Map.get(assigns, :name) assigns = assigns |> Map.put_new(:class, nil) |> Map.put(:__component_name, component_name) |> Map.put(:props, props) |> Map.put(:slots, if(slots_changed?, do: Slots.rendered_slot_map(slots), else: %{})) assigns = Map.put(assigns, :ssr_render, if(render_ssr?, do: ssr_render(assigns), else: nil)) computed_changed = %{ props: props_changed?, slots: slots_changed?, ssr_render: render_ssr? } assigns = update_in(assigns.__changed__, fn nil -> nil changed -> for {k, true} <- computed_changed, into: changed, do: {k, true} end) # It's important to not add extra `\n` in the inner div or it will break hydration ~H"""
Slots.base_encode_64 |> json}"} data-ssr={is_map(@ssr_render)} phx-update="ignore" phx-hook="ReactHook" class={@class} ><%= raw(@ssr_render[:html]) %>
""" end defp extract(assigns, type) do Enum.reduce(assigns, {%{}, false}, fn {key, value}, {acc, changed} -> case normalize_key(key, value) do ^type -> {Map.put(acc, key, value), changed || key_changed(assigns, key)} _ -> {acc, changed} end end) end defp normalize_key(key, _val) when key in ~w(id class ssr name socket __changed__ __given__)a, do: :special defp normalize_key(_key, [%{__slot__: _}]), do: :slots defp normalize_key(key, val) when is_atom(key), do: key |> to_string() |> normalize_key(val) defp normalize_key(_key, _val), do: :props defp key_changed(%{__changed__: nil}, _key), do: true defp key_changed(%{__changed__: changed}, key), do: changed[key] != nil defp ssr_render(assigns) do try do name = Map.get(assigns, :name) SSR.render(name, assigns.props, assigns.slots) rescue SSR.NotConfigured -> nil end end defp json(data), do: Jason.encode!(data, escape: :html_safe) defp id(name) do # a small trick to avoid collisions of IDs but keep them consistent across dead and live render # id(name) is called only once during the whole LiveView lifecycle because it's not using any assigns number = Process.get(:live_react_counter, 1) Process.put(:live_react_counter, number + 1) "#{name}-#{number}" end end