1
<div id="counter-a" phx-hook="CounterHook" start={@count} />
This is what I meant by keeping the API consistent with how you would use a Phoenix Component using Phoenix Hooks.
assets/js/counter.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import React, { useEffect, useState } from "react";
import { createRoot } from "react-dom/client";
const CounterComponent = ({ id, send, onReceive, start }) => {
const [count, setCount] = useState(start);
const [serverMessage, setServerMessage] = useState("");
useEffect(() => {
onReceive(`even`, (event) => {
setServerMessage(event.msg);
});
onReceive(`odd`, (event) => {
setServerMessage("");
});
}, []);
return (
<div className="flex flex-row gap-8 items-center ">
<button
className="border-2 rounded-md px-4 py-2 bg-lime-300"
onClick={() => setCount(count + 1)}
>
+
</button>
<p className="text-lg">{count}</p>
<button
className="border-2 rounded-md px-4 py-2 bg-lime-300"
onClick={() => setCount(count + 1)}
>
-
</button>
<button
onClick={() => send("count-update", { id, count }}
>
Send To server
</button>
<p className="text-red-600">{serverMessage}</p>
</div>
);
};
export var CounterHook = {
mounted() {
let el = this.el;
let id = el.getAttribute("id");
let start = parseInt(el.getAttribute("start"));
const root = createRoot(el);
root.render(
<CounterComponent
id={id}
start={start}
send={this.pushEvent.bind(this)}
onReceive={this.handleEvent.bind(this)}
/>
);
},
};
Some Notable points about the Hook :
mounted
function we use client side javascript to parse attributes, format them and pass them as props to a React Component.onReceive
and send
to be passed to every React Component so that it can communicate directly with the liveview process by sending and receiving events.id
prop, although its not enforced anywhere yet. This can be useful when you have multiple instances of the same component and you want to send an event from the server to any particular one of them.Some Notable points about the React Component
1
<button onClick={() => send("count-update", { id, count }) > Send to server </button>
This looks a bit like the equivalent we have grown used to in heex.
1
<button phx-click{"count-update"}>Send to server</button>
1
2
3
4
5
6
7
8
9
10
11
12
def handle_event("count-update", params, socket) do
count = params["count"]
id = params["id"]
socket =
case rem(count, 2) do
0 -> push_event(socket, "even", %{msg: "even"})
1 -> push_event(socket, "odd", %{msg: "odd"})
end
{:noreply, socket}
end
This should be familiar to anyone who has used phoenix hooks.
1
2
3
4
5
6
7
8
9
10
11
12
import { CounterHook } from "./counter";
let Hooks = {
CounterHook,
... any other hooks you may have
};
let liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: { _csrf_token: csrfToken },
hooks: Hooks,
});
This part is new. In your config.exs, you need to add the loader flag - --loader:.js=jsx to the args option. The full value should look like
1
args: ~w(js/app.js --loader:.js=jsx --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*)
You are good to go now.
There are many places where you could abstract some things out to reduce code redundancy. Thats probably also why libraries like live_react exist and you should use them if apt. I like that this approach lets you add React components to an existing Phoenix project without adding any aditional dependencies. I also like that counter.js contains both the Phoenix Hook and the React Component. This makes the counter feel like a Phoenix component that handles markup, style, interactivity and communication with liveview; all in one file.
Full code is available as a gist.