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