diff --git a/assets/js/live_react/context.jsx b/assets/js/live_react/context.jsx new file mode 100644 index 00000000..94eeed94 --- /dev/null +++ b/assets/js/live_react/context.jsx @@ -0,0 +1,15 @@ +import React, { createContext, useContext } from "react"; + +export const LiveReactContext = createContext(null); + +export function LiveReactProvider({ children, ...props }) { + return ( + + {children} + + ); +} + +export function useLiveReact() { + return useContext(LiveReactContext); +} diff --git a/assets/js/live_react/hooks.js b/assets/js/live_react/hooks.js index 48e5b924..b43b3906 100644 --- a/assets/js/live_react/hooks.js +++ b/assets/js/live_react/hooks.js @@ -1,5 +1,6 @@ import React from "react"; import ReactDOM from "react-dom/client"; +import { getComponentTree } from "./utils"; function getAttributeJson(el, attributeName) { const data = el.getAttribute(attributeName); @@ -23,7 +24,6 @@ function getChildren(hook) { function getProps(hook) { return { ...getAttributeJson(hook.el, "data-props"), - // pass the hook callbacks to the component pushEvent: hook.pushEvent.bind(hook), pushEventTo: hook.pushEventTo.bind(hook), handleEvent: hook.handleEvent.bind(hook), @@ -36,13 +36,12 @@ function getProps(hook) { export function getHooks(components) { const ReactHook = { _render() { - this._root.render( - React.createElement( - this._Component, - getProps(this), - ...getChildren(this), - ), + const tree = getComponentTree( + this._Component, + getProps(this), + getChildren(this), ); + this._root.render(tree); }, mounted() { const componentName = this.el.getAttribute("data-name"); @@ -55,14 +54,12 @@ export function getHooks(components) { const isSSR = this.el.hasAttribute("data-ssr"); if (isSSR) { - this._root = ReactDOM.hydrateRoot( - this.el, - React.createElement( - this._Component, - getProps(this), - ...getChildren(this), - ), + const tree = getComponentTree( + this._Component, + getProps(this), + getChildren(this), ); + this._root = ReactDOM.hydrateRoot(this.el, tree); } else { this._root = ReactDOM.createRoot(this.el); this._render(); diff --git a/assets/js/live_react/index.mjs b/assets/js/live_react/index.mjs index 999e7b6e..2976da72 100644 --- a/assets/js/live_react/index.mjs +++ b/assets/js/live_react/index.mjs @@ -1 +1,2 @@ export { getHooks } from "./hooks"; +export { useLiveReact } from "./context"; diff --git a/assets/js/live_react/server.mjs b/assets/js/live_react/server.mjs index 35260dd1..ac31f9ab 100644 --- a/assets/js/live_react/server.mjs +++ b/assets/js/live_react/server.mjs @@ -1,8 +1,17 @@ import React from "react"; import { renderToString } from "react-dom/server"; +import { getComponentTree } from "./utils"; -function Wrapper({ children }) { - return React.createElement(React.Fragment, null, children); +function getChildren(slots) { + if (!slots?.default) { + return []; + } + + return [ + React.createElement("div", { + dangerouslySetInnerHTML: { __html: slots.default.trim() }, + }), + ]; } export function getRender(components) { @@ -11,25 +20,10 @@ export function getRender(components) { if (!Component) { throw new Error(`Component "${name}" not found`); } - - let children = []; - if (slots?.default) { - children.push( - React.createElement("div", { - dangerouslySetInnerHTML: { __html: slots.default.trim() }, - }), - ); - } - - // The Component need to be wrapped to prevent useState useEffect error which can't be root component - const componentInstance = React.createElement( - Component, - props, - ...children, - ); - const content = React.createElement(Wrapper, null, componentInstance); + const children = getChildren(slots); + const tree = getComponentTree(Component, props, children); // https://react.dev/reference/react-dom/server/renderToString - return renderToString(content); + return renderToString(tree); }; } diff --git a/assets/js/live_react/utils.js b/assets/js/live_react/utils.js new file mode 100644 index 00000000..3f3e2708 --- /dev/null +++ b/assets/js/live_react/utils.js @@ -0,0 +1,23 @@ +import React from "react"; +import { LiveReactProvider } from "./context"; + +function getHooks(props) { + return { + pushEvent: props.pushEvent, + pushEventTo: props.pushEventTo, + handleEvent: props.handleEvent, + removeHandleEvent: props.removeHandleEvent, + upload: props.upload, + uploadTo: props.uploadTo, + }; +} + +export function getComponentTree(Component, props, children) { + const componentInstance = React.createElement(Component, props, ...children); + + return React.createElement( + LiveReactProvider, + getHooks(props), + componentInstance, + ); +} diff --git a/live_react_examples/assets/react-components/context.jsx b/live_react_examples/assets/react-components/context.jsx new file mode 100644 index 00000000..510a07de --- /dev/null +++ b/live_react_examples/assets/react-components/context.jsx @@ -0,0 +1,38 @@ +import React, { useState } from "react"; +import { useLiveReact } from "live_react"; + +export function Context({ count }) { + const [amount, setAmount] = useState(1); + const { pushEvent, ...rest } = useLiveReact(); + console.log(rest); + + return ( +
+
+ + {count} + +
+ +
+ ); +} diff --git a/live_react_examples/assets/react-components/counter.jsx b/live_react_examples/assets/react-components/counter.jsx index 66c4d122..9a8835ed 100644 --- a/live_react_examples/assets/react-components/counter.jsx +++ b/live_react_examples/assets/react-components/counter.jsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; -export function Counter({ count, onInc, onDec }) { +export function Counter({ count }) { const [amount, setAmount] = useState(1); return ( diff --git a/live_react_examples/assets/react-components/index.jsx b/live_react_examples/assets/react-components/index.jsx index 14d1377c..c70e05c3 100644 --- a/live_react_examples/assets/react-components/index.jsx +++ b/live_react_examples/assets/react-components/index.jsx @@ -1,6 +1,7 @@ // polyfill recommended by Vite https://vitejs.dev/config/build-options#build-modulepreload import "vite/modulepreload-polyfill"; +import { Context } from "./context"; import { Counter } from "./counter"; import { DelaySlider } from "./delay-slider"; import { FlashSonner } from "./flash-sonner"; @@ -14,6 +15,7 @@ import { Slot } from "./slot"; import { Typescript } from "./typescript"; export default { + Context, Counter, DelaySlider, FlashSonner, diff --git a/live_react_examples/lib/live_react_examples.ex b/live_react_examples/lib/live_react_examples.ex index e17719df..60c7b4ba 100644 --- a/live_react_examples/lib/live_react_examples.ex +++ b/live_react_examples/lib/live_react_examples.ex @@ -116,6 +116,16 @@ defmodule LiveReactExamples do } end + def demo(:context) do + %{ + raw_view_url: "#{@raw_url}#{@live_views}/context.ex", + view_url: "#{@url}#{@live_views}/context.ex", + view_language: "elixir", + raw_react_url: "#{@raw_url}#{@react}/context.jsx", + react_url: "#{@url}#{@react}/context.jsx" + } + end + def demo(demo) do raise ArgumentError, "Unknown demo: #{inspect(demo)}" end diff --git a/live_react_examples/lib/live_react_examples_web/components/layouts/app.html.heex b/live_react_examples/lib/live_react_examples_web/components/layouts/app.html.heex index cd554a07..1840d476 100644 --- a/live_react_examples/lib/live_react_examples_web/components/layouts/app.html.heex +++ b/live_react_examples/lib/live_react_examples_web/components/layouts/app.html.heex @@ -95,6 +95,12 @@ > Slot + <.link + class="group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline font-medium text-zinc-700" + navigate={~p"/context"} + > + Context + diff --git a/live_react_examples/lib/live_react_examples_web/live/context.ex b/live_react_examples/lib/live_react_examples_web/live/context.ex new file mode 100644 index 00000000..a1326c94 --- /dev/null +++ b/live_react_examples/lib/live_react_examples_web/live/context.ex @@ -0,0 +1,23 @@ +defmodule LiveReactExamplesWeb.LiveContext do + use LiveReactExamplesWeb, :live_view + + def render(assigns) do + ~H""" +

Hybrid: LiveView + React

+ <.react name="Context" count={@count} socket={@socket} ssr={true} /> + """ + end + + def mount(_session, _params, socket) do + {:ok, assign(socket, :count, 10)} + end + + # def handle_event("set_count", params, socket) do + # IO.inspect(params) + # {:noreply, socket} + # end + + def handle_event("set_count", %{"value" => number}, socket) do + {:noreply, assign(socket, :count, number)} + end +end diff --git a/live_react_examples/lib/live_react_examples_web/live/demo_assigns.ex b/live_react_examples/lib/live_react_examples_web/live/demo_assigns.ex index eb8a563d..5d30497e 100644 --- a/live_react_examples/lib/live_react_examples_web/live/demo_assigns.ex +++ b/live_react_examples/lib/live_react_examples_web/live/demo_assigns.ex @@ -32,6 +32,9 @@ defmodule LiveReactExamplesWeb.LiveDemoAssigns do {LiveReactExamplesWeb.LiveSlot, _} -> :slot + {LiveReactExamplesWeb.LiveContext, _} -> + :context + {_view, _live_action} -> nil end diff --git a/live_react_examples/lib/live_react_examples_web/router.ex b/live_react_examples/lib/live_react_examples_web/router.ex index a6998f2b..a49d6db6 100644 --- a/live_react_examples/lib/live_react_examples_web/router.ex +++ b/live_react_examples/lib/live_react_examples_web/router.ex @@ -24,6 +24,7 @@ defmodule LiveReactExamplesWeb.Router do get "/typescript", PageController, :typescript live "/live-counter", LiveCounter + live "/context", LiveContext live "/log-list", LiveLogList live "/flash-sonner", LiveFlashSonner live "/ssr", LiveSSR