Integrate React18 with LiveView

Did anyone integrate React18 with LiveView? I know this is an Elixir/Phoenix forum, and I am not sure this is an Elixir problem, but I imagine that the mobile market - thus React Native - is so big that I may find some experience here. So, with the new React 18 api, I have multiple renderings, and it’s not the StrictMode issue. I am curious because I have to return the root from the component to make it work. The Liveview docs are pretty dry on this. I used mounted, push_event and pushEventTo.

In any case, I can share the demo code: I tried two different approaches:

  • an event sends data to the server to mutate it and is sent back to the client for rendering,
  • an event mutates the data client side and the data is sent to the server to display outside of the component for example.
    Both give multiple renderings (the logs)
export function mountCounters(id, opts) {
  console.log('mount');
  const container = document.getElementById(id);
  const root = createRoot(container);
  root.render(
    <React.StrictMode>
      <Counters {...opts} />
    </React.StrictMode>
  );

  return root; // <- ?
}
export const CounterHook = {
  CounterA: {
    mounted() {
      const root = mountCounters('a', this.props());

      this.handleEvent('updateCount', ({ newCount: newCount, inc: inc }) => {
        const props = this.props(newCount, inc);
        root.render(<Counters {...props} />);
      ^^^
      });
    },
    props(counter = 0, inc = 5) {
      return {
        inc: inc,
        count: counter,
        sendMsg: () =>
          this.pushEventTo('a', 'incCount', {
            counter: counter,
            inc: inc,
          }),
        pushMsg: c => this.pushEventTo('a', 'incremented', { counter: c }),
      };
    },
  },
};
export const Counters = ({ inc, sendMsg, pushMsg, count = 0 }) => {
  const [counter, setCounter] = React.useState(0);
  const incCounter = () => {
    setCounter(counter => counter + inc);
    pushMsg(counter + inc);
  };

  console.log(counter, count);
  return (
    <section className='phx-hero'>
      <h1>SSR of the value of the counter</h1>
      <button onClick={sendMsg}>+{inc}</button>
      <br />
      <p>{count}</p>
      <br />
      <hr />
      <h1>React rendered counter in component</h1>
      <button onClick={incCounter}>+{inc}</button>
      <br />
      <span>{counter}</span>
    </section>
  );
};
def mount(_params, _session, socket) do
    socket = socket |> assign(count: 0, value:  0)
    {:ok, socket}
  end

  def handle_event("incCount", %{"counter" => counter, "inc" => inc}, socket) do
    IO.puts("push")
    new_count = counter + inc

    socket = assign(socket, count: new_count, inc: inc)

    {:noreply, push_event(socket, "updateCount", %{newCount: new_count, inc: inc})}
  end

  def handle_event("incremented", %{"counter" => counter}, socket) do
    IO.puts("receive")
    IO.inspect(counter, label: "value")
    socket = assign(socket, :value, counter)
    {:noreply, socket}
  end

  def render(assigns) do
    IO.puts("render")
    ~H"""
    <div id="a" phx-hook="CounterA" phx-update="ignore"></div>
    <h1>Pushed by React from the component: <%= @value %></h1>
    """
  end

I’m not sure what your question is here exactly, if any.

I’ve taken a similar approach, and used GitHub - pmndrs/zustand: 🐻 Bear necessities for state management in React for state management, something like this. This example is super basic, as it sends every state change completely, but it should give you an idea on how to integrate.

    mounted() {
      let { setState, subscribe } = someStore;
      setState((state) => JSON.parse(this.el.dataset.design));
      subscribe((state) => {
        this.pushEvent("update-state", state);
      });
      const root = document.getElementById("root");


        ReactDOM.createRoot(root).render(
          <React.StrictMode>
            <App />
          </React.StrictMode>
        );
    },

This is interesting, but did you think of reversing the approach?

There are several problems with these frontend-driven applications, and their state management, that I really really like LiveView not having:

  1. state lives and is controlled on the server. Meaning, the state operations can understand user permissions, constraints etc.

  2. state can be populated entirely on the server-side, without the need to become served as a JSON API or GraphQL or dom attributes

Now I think the reversed approach would be never to update state on the client side, but capture events, send it to the backend, where state gets updated and is pushed back to the client. Something like JSON diff would be used to minimize the amount of data being sent back, and then JSON patch restores the state.

To be clear, the above are just toy/demo things that I’ve played with, but it works well.

I haven’t had the use for that yet, but I don’t think it should be hard to do. You would send actions (I wouldn’t call them events because the server still has to decide what the outcome of the action is) from the frontend to the backend, and backend responds with either events (in that case, the frontend is responsible to calculate the new state from the events) or with state changes.

In that case the use case for React (or any frontend framework) becomes much smaller though, because you’re doing something similar to LiveView. So the only reason I’d do it is if you have some frontend components that are not available in LiveView (or it might be a migration path towards LiveView).

In my current approach you have the advantage of the frontend having optimistic updates and thus lower latency if that is something you need.

Well, I have slightly different thoughts. LiveView is great but there are several disadvantages, and that everything is in Elixir is actually one of them, as it’s easier to find JavaScript developers rather than Elixir developers.

Second reason why you would like to have React responsible for rendering things is availability of UI libraries.

Third reason why you would like to have React responsible for rendering the UI is that in certain cases it is, and probably always will be, more efficient than LiveView, because of VirtualDOM with key attribute on the list items, things like updates to lists are both easier and more performant to do.

Ideally I’d keep React myself as I really do like it, but cut out the middleman in form of API/GraphQL and handle the state on the server.

And I do agree this is probably not too complicated to do.

1 Like

Ah yes, thanks! you use a store manager, I will try this!
The problem is that some functions server-side trigger multiple times as seen with the IO.puts, so this is wrong.
I wanted to evaluate 2 use cases: firstly propagate to the server a state change client side, and secondly, send data from the client side for the server to do something with it and broadcast back some answer. I used a counter as a demo to start but in reality, it is markers/positions on a map that should be partly live broadcasted.
You might not want to spend CPU server-side for things that can be done faster and smother client-side.

1 Like

Although it is not React, but web components, You might find this useful…

You might also find all the hooks tools to build your own state management, with the help of useContext, useReducer and custom hooks. React hooks, not liveview hooks.

I use condition to test if the root node exists, because it might not be present on all pages…

if(root) {
  ...
}

Yes, I will watch the video. Lit elements works well but I was just a bit allergic to the old-fashioned Javasoup “classes” with this bind etc, close to abstract nonsense for me :frowning: . . Strangely enough, the Liveview code uses classes

You might also find all the hooks tools to build your own state management, with the help of useContext, useReducer and custom hooks. React hooks, not liveview hooks.

I will try valtio from the same guy who made Zudstand cited by tcoopman. Far more digestible than Redux and you can mutate the state, much less verbose.

IIRC The demo is about building maps, with liveview and webcomponents.

I am not speaking of Redux, but React Hooks :slight_smile:
You can store state AND actions to modify the state.
You can build your own usePhx, and use it in any component like this

const MyComp = () => {
  const {state, actions} = usePhx()
  return ...
}

and have access to helper functions for joining a socket, connecting/leaving a channel

… and the author of React Three Fiber

Highly recommended if You like ThreeJs and React.

It was a stupid mistake: I used pushEventTo('a',...) where a is the DOM selector because of misreading this part of the doc

the selectorOrTarget is defined in, where its value can be either a query selector or an actual DOM element

When I used this.el instead, or simply pushEvent, it worked. Now both examples work, whether you keep the state in the genserver or with React (a state manager would be the same but I still used the props to pass the functions, bug with Valtio)

In case of any interset, a simple example that works:

The hook:

export const Hook={}
Hook.Counter = {
  mounted() {
    this.handleEvent(
      'from_server',
      ({ newCount: newCount }) => (this.count = newCount)
    );
   let container;
   if ((container = document.getElementById('a'))) {
      createRoot(container).render(
        <Counters push={c => this.push(c)} ssr={() => this.ssr()} />
      );
  },
  count: 0,
  push(c) {
    this.pushEvent('from_client', { counter: c });
  },
  ssr() {
    this.pushEvent('ssr', {});
  },
};

Alternatively, with a callback in this.pushEvent('ssr', (}, ({newCount})=>(this.count = newCount)) and you remove the handleEvent but the “handle_event” should {;reply, %{newCount; new_count}, socket}

export function Counters({ push, ssr }) {
  // React ->
  const [value, setValue] = React.useState(0);
  const action = () => {
    setValue(value => value + 10);
    push(value + 10);
  };
// <- end React

  return (
    <>
      <h1>React</h1>
      <button onClick={action}>+10</button>
      <br />
      <p>{value}</p>
      <br />
     <h1>SSR</h1>
      <button onClick={ssr}>+5</button>
    </>
  );
}

and the Liveview:

  def mount(_params, _session, socket) do
    {:ok, assign(socket, count: 0, value: 0)}
  end

  def handle_event("ssr", _, socket) do
    new_count = socket.assigns.count + 5
    socket = assign(socket, count: new_count, value: new_count)
    {:noreply, push_event(socket, "from_server", %{newCount: new_count})}
  end

  def handle_event("from_client", %{"counter" => counter}, socket) do
    IO.puts("receive")
    IO.inspect(counter)
    {:noreply, assign(socket, value: counter, count: counter)}
  end

  def render(assigns) do
    ~H"""
    <div id="a" phx-hook="Counter" phx-update="ignore"  ></div>
    <h1>Counter: <%= @value %></h1>
    """
  end

The demo is about building maps, with liveview and webcomponents

Yes, thanks, exactly the subject, very interesting, and nice usage of Postgis.

and have access to helper functions for joining a socket, connecting/leaving a channel

Can you point me where are these helper functions? Thks.