Skip to content

Commit 2c76868

Browse files
committed
feat: add react in phoenix blog
1 parent 2c0491a commit 2c76868

File tree

2 files changed

+165
-0
lines changed

2 files changed

+165
-0
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
---
2+
name: Add React Components to a Phoenix App without adding additional dependencies
3+
excerpt: When you want to sprinkle a little interactivity without pulling in opaque dependencies
4+
author: Denny George
5+
project: ""
6+
date: 2025-02-23
7+
tags: devlog
8+
cover: ../images/react-component-in-phoenix.png
9+
---
10+
11+
# Problem :
12+
13+
- We need SOME client side interactivity
14+
- Our team has frontend developers who know react
15+
- React has battle tested components
16+
- I didn't want to add additional dependencies to our base phoenix project
17+
- I wanted a mental model consistent with phoenix's way of working - heex, hooks and events
18+
19+
![](../images/react-component-in-phoenix.png)
20+
21+
# Solution :
22+
23+
## Insert the following heex markup in your liveview code
24+
25+
```heex
26+
<div id="counter-a" phx-hook="CounterHook" start={@count} />
27+
```
28+
29+
This is what I meant by keeping the API consistent with how you would use a Phoenix Component using Phoenix Hooks.
30+
31+
## Create a file for your React Component
32+
33+
<code>assets/js/counter.js</code>
34+
```jsx
35+
import React, { useEffect, useState } from "react";
36+
import { createRoot } from "react-dom/client";
37+
38+
const CounterComponent = ({ id, send, onReceive, start }) => {
39+
const [count, setCount] = useState(start);
40+
const [serverMessage, setServerMessage] = useState("");
41+
42+
useEffect(() => {
43+
onReceive(`even`, (event) => {
44+
setServerMessage(event.msg);
45+
});
46+
47+
onReceive(`odd`, (event) => {
48+
setServerMessage("");
49+
});
50+
}, []);
51+
52+
return (
53+
<div className="flex flex-row gap-8 items-center ">
54+
<button
55+
className="border-2 rounded-md px-4 py-2 bg-lime-300"
56+
onClick={() => setCount(count + 1)}
57+
>
58+
+
59+
</button>
60+
<p className="text-lg">{count}</p>
61+
<button
62+
className="border-2 rounded-md px-4 py-2 bg-lime-300"
63+
onClick={() => setCount(count + 1)}
64+
>
65+
-
66+
</button>
67+
<button
68+
onClick={() => send("count-update", { id, count }}
69+
>
70+
Send To server
71+
</button>
72+
<p className="text-red-600">{serverMessage}</p>
73+
</div>
74+
);
75+
};
76+
77+
export var CounterHook = {
78+
mounted() {
79+
let el = this.el;
80+
let id = el.getAttribute("id");
81+
let start = parseInt(el.getAttribute("start"));
82+
83+
const root = createRoot(el);
84+
root.render(
85+
<CounterComponent
86+
id={id}
87+
start={start}
88+
send={this.pushEvent.bind(this)}
89+
onReceive={this.handleEvent.bind(this)}
90+
/>
91+
);
92+
},
93+
};
94+
95+
```
96+
97+
Some Notable points about the Hook :
98+
99+
1. In the <code>mounted</code> function we use client side javascript to parse attributes, format them and pass them as props to a React Component.
100+
2. We mandate two props <code>onReceive</code> and <code>send</code> to be passed to every React Component so that it can communicate directly with the liveview process by sending and receiving events.
101+
3. Sending an <code>id</code> 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.
102+
103+
Some Notable points about the React Component
104+
105+
1. Its familiar to react developers
106+
2. We continue using tailwind for styling. This is a big win because I value being able to style my components in accordance with the rest of our app; so since we already use tailwind for the rest of the UI components, this is great.
107+
3. To send events from the component to liveview, we use
108+
```jsx
109+
<button onClick={() => send("count-update", { id, count }) > Send to server </button>
110+
```
111+
This looks a bit like the equivalent we have grown used to in heex.
112+
```heex
113+
<button phx-click{"count-update"}>Send to server</button>
114+
```
115+
4. We use useEffect() to setup listeners for server events.
116+
117+
## Add an event handler in your liveview
118+
119+
```ex
120+
def handle_event("count-update", params, socket) do
121+
count = params["count"]
122+
id = params["id"]
123+
124+
socket =
125+
case rem(count, 2) do
126+
0 -> push_event(socket, "even", %{msg: "even"})
127+
1 -> push_event(socket, "odd", %{msg: "odd"})
128+
end
129+
130+
{:noreply, socket}
131+
end
132+
```
133+
134+
## Register the hook in app.js
135+
136+
This should be familiar to anyone who has used phoenix hooks.
137+
138+
```js
139+
import { CounterHook } from "./counter";
140+
141+
let Hooks = {
142+
CounterHook,
143+
... any other hooks you may have
144+
};
145+
146+
let liveSocket = new LiveSocket("/live", Socket, {
147+
longPollFallbackMs: 2500,
148+
params: { _csrf_token: csrfToken },
149+
hooks: Hooks,
150+
});
151+
```
152+
153+
## Change esbuild configuration to support JSX
154+
155+
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
156+
```ex
157+
args: ~w(js/app.js --loader:.js=jsx --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*)
158+
```
159+
160+
You are good to go now.
161+
162+
## Conclusion :
163+
There are many places where you could abstract some things out to reduce code redundancy. Thats probably also why libraries like [live_react](https://github.com/mrdotb/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.
164+
165+
Full code is available as a [gist](https://gist.github.com/dennyabrain/a7056d1f4912ab63d5a3ee331105cd29).
17.7 KB
Loading

0 commit comments

Comments
 (0)