diff --git a/assets/js/live_react/hooks.js b/assets/js/live_react/hooks.js index dd98b5bb..168ef6f0 100644 --- a/assets/js/live_react/hooks.js +++ b/assets/js/live_react/hooks.js @@ -124,13 +124,11 @@ export function getHooks(components) { } }, destroyed() { - if (this._root) { - window.addEventListener( - "phx:page-loading-stop", - () => this._root.unmount(), - { once: true }, - ); - } + const root = this._root; + if (!root) return; + + this._root = null; + root.unmount(); }, }; diff --git a/assets/js/live_react/hooks.test.js b/assets/js/live_react/hooks.test.js index 3d203da1..addabc2b 100644 --- a/assets/js/live_react/hooks.test.js +++ b/assets/js/live_react/hooks.test.js @@ -64,6 +64,7 @@ describe("ReactHook", () => { beforeEach(async () => { vi.resetModules(); renderMock.mockClear(); + rootMock.unmount.mockClear(); ({ getHooks } = await import("./hooks")); ({ ReactHook } = getHooks({ TestComponent })); }); @@ -219,4 +220,22 @@ describe("ReactHook", () => { expect(lastRenderedProps().title).toBe("World"); }); + + it("destroyed() immediately unmounts the root exactly once and clears it", () => { + const hook = createMockLiveViewHook({ + "data-name": "TestComponent", + "data-props": encodeProps({ title: "Hello" }), + }); + + ReactHook.mounted.call(hook); + ReactHook.destroyed.call(hook); + + expect(rootMock.unmount).toHaveBeenCalledTimes(1); + expect(hook._root).toBeNull(); + + window.dispatchEvent(new Event("phx:page-loading-stop")); + ReactHook.destroyed.call(hook); + + expect(rootMock.unmount).toHaveBeenCalledTimes(1); + }); }); diff --git a/lib/live_react.ex b/lib/live_react.ex index c5211def..65e25dc0 100644 --- a/lib/live_react.ex +++ b/lib/live_react.ex @@ -13,8 +13,6 @@ defmodule LiveReact do alias Phoenix.LiveView alias Phoenix.LiveView.LiveStream - require Logger - @ssr_default Application.compile_env(:live_react, :ssr, true) @diff_default Application.compile_env(:live_react, :enable_props_diff, true) diff --git a/lib/live_react/encoder.ex b/lib/live_react/encoder.ex index 86c02890..ff83a31b 100644 --- a/lib/live_react/encoder.ex +++ b/lib/live_react/encoder.ex @@ -52,33 +52,44 @@ defprotocol LiveReact.Encoder do @doc """ Encodes a value to one of the primitive types. """ + @spec encode(t) :: any() + def encode(value) + @spec encode(t, opts) :: any() - def encode(value, opts \\ []) + def encode(value, opts) end defimpl LiveReact.Encoder, for: Integer do + def encode(value), do: encode(value, []) def encode(value, _opts), do: value end defimpl LiveReact.Encoder, for: Float do + def encode(value), do: encode(value, []) def encode(value, _opts), do: value end defimpl LiveReact.Encoder, for: BitString do + def encode(value), do: encode(value, []) def encode(value, _opts), do: value end defimpl LiveReact.Encoder, for: Atom do + def encode(value), do: encode(value, []) def encode(atom, _opts), do: atom end defimpl LiveReact.Encoder, for: List do + def encode(value), do: encode(value, []) + def encode(list, opts) do Enum.map(list, &LiveReact.Encoder.encode(&1, opts)) end end defimpl LiveReact.Encoder, for: Map do + def encode(value), do: encode(value, []) + def encode(map, opts) do Map.new(map, fn {key, value} -> {key, LiveReact.Encoder.encode(value, opts)} @@ -87,6 +98,8 @@ defimpl LiveReact.Encoder, for: Map do end defimpl LiveReact.Encoder, for: [Date, Time, NaiveDateTime, DateTime] do + def encode(value), do: encode(value, []) + def encode(value, _opts) do @for.to_iso8601(value) end @@ -98,6 +111,8 @@ defimpl LiveReact.Encoder, for: Any do quote do defimpl LiveReact.Encoder, for: unquote(module) do + def encode(value), do: encode(value, []) + def encode(struct, opts) do struct |> Map.take(unquote(fields)) @@ -107,6 +122,8 @@ defimpl LiveReact.Encoder, for: Any do end end + def encode(value), do: encode(value, []) + def encode(%{__struct__: module} = struct, _opts) do raise Protocol.UndefinedError, protocol: @protocol, @@ -178,6 +195,8 @@ defimpl LiveReact.Encoder, for: Any do end defimpl LiveReact.Encoder, for: Phoenix.LiveView.AsyncResult do + def encode(value), do: encode(value, []) + def encode(%Phoenix.LiveView.AsyncResult{} = struct, opts) do LiveReact.Encoder.encode( %{ @@ -196,6 +215,8 @@ defimpl LiveReact.Encoder, for: Phoenix.LiveView.AsyncResult do end defimpl LiveReact.Encoder, for: Phoenix.LiveView.UploadConfig do + def encode(value), do: encode(value, []) + def encode(%Phoenix.LiveView.UploadConfig{} = struct, opts) do errors = Enum.map(struct.errors, fn {key, value} -> @@ -225,6 +246,8 @@ defimpl LiveReact.Encoder, for: Phoenix.LiveView.UploadConfig do end defimpl LiveReact.Encoder, for: Phoenix.LiveView.UploadEntry do + def encode(value), do: encode(value, []) + def encode(%Phoenix.LiveView.UploadEntry{} = struct, opts) do LiveReact.Encoder.encode( %{ @@ -243,6 +266,8 @@ defimpl LiveReact.Encoder, for: Phoenix.LiveView.UploadEntry do end defimpl LiveReact.Encoder, for: Phoenix.HTML.Form do + def encode(value), do: encode(value, []) + def encode(%Phoenix.HTML.Form{} = form, opts) do LiveReact.Encoder.encode( %{ @@ -258,9 +283,9 @@ defimpl LiveReact.Encoder, for: Phoenix.HTML.Form do defp get_form_validity(%{source: %{valid?: valid}}), do: valid defp get_form_validity(_), do: true - if Code.ensure_loaded?(Ecto) do - @relations [:embed, :assoc] + @relations [:embed, :assoc] + if Code.ensure_loaded?(Ecto) and Code.ensure_loaded?(Phoenix.HTML.FormData.Ecto.Changeset) do defp collect_changeset_values(%Ecto.Changeset{} = source, opts) do data = Map.new(source.types, fn {field, type} -> diff --git a/lib/live_react/patch.ex b/lib/live_react/patch.ex index dd4d3e82..8972723d 100644 --- a/lib/live_react/patch.ex +++ b/lib/live_react/patch.ex @@ -190,7 +190,7 @@ defmodule LiveReact.Patch do defp take_js_string(payload, length), do: take_js_string(payload, payload, length, 0) defp take_js_string(original, _rest, 0, bytes) do - <> = original + <> = original {value, rest} end diff --git a/lib/live_react/ssr.ex b/lib/live_react/ssr.ex index 49345dc8..5c025244 100644 --- a/lib/live_react/ssr.ex +++ b/lib/live_react/ssr.ex @@ -5,8 +5,6 @@ defmodule LiveReact.SSR.NotConfigured do end defmodule LiveReact.SSR do - require Logger - @moduledoc """ A behaviour for rendering React components server-side.