React in LiveView: How and Why?

I have been using LiveView daily for about 2 years now. It’s honestly pretty much amazing for my needs. But, sometimes I have a complex need that already has a React library for it. Luckily, integrations between LiveView and React are pretty easy to setup.

I’m going to walk through what that looks like.

Use Case Examples (Why)

Let me set some foundation for what my application does, and where React/LiveView have been effective for me in the past.

Clove customers use our backend UI (powered by LiveView) to do things like drag-and-drop elements onto a “canvas” (used in the creative sense), write content with a WYSIWYG editor, and bulk import data from CSVs. Of course, there’s the boring things like list views, update forms, and other CRUD-like activities.

Here’s an example of our “Success Space” feature to show a use case for when I have used React. The entire editor is powered by the amazing React Grid Layout. (This is not an sly ad for Clove, I just happened to have this video showing off the feature.)

I’ve used React for this feature and the others mentioned above. I personally don’t use React with client-side routes. I leave that in LiveView and just use React for components.

Guidelines for LiveView + React

I mention above that I don’t use React client-side routes. It might be helpful for me to put out my ground “rules” for how I integrate the systems. These are not actual rules that you must follow, they’re just guidelines that I follow to keep my integration clean.

Also, I’m sure there’s more than these three guidelines. They’re just things that came to mind as most important when thinking about this.

React components are leaf-nodes in the DOM

When integrating two frontend frameworks, I find that going back/forth between them is a recipe for disaster. For example, going LiveView -> React -> LiveView would likely be rough in the long run. So, I have a rule that React elements can’t embed LiveView underneath them.

Bidirectional data flow should be over the WebSocket

Passing data back/forth from LiveView <-> React is an important part of useful applications. When I do this type of communication, I make sure to define a clean layer of actions that are available to the React application. You’ll see this later in the article.

I do not make HTTP API calls from my React components, and I would highly suggest avoiding that. I think it creates an increase of complexity that probably isn’t worth it.

Think of React components as self-contained applications

When I’m writing React components to be used in LiveView, I always think of them as completely self-contained. This means that I could take the component and put it somewhere else by simply defining the right props going into React.

The benefit of this is that I can (and have) embed React applications built for other use cases (like a Chrome Extension) into my LiveView app very easily. Or I can take a React application built in LiveView and take it somewhere else with ease.

Embedding React Components

Okay, here’s finally the meaty code-portion of the post. Here’s the formula I follow for my React components. I use TypeScript as well, so you will see some of those annotations. I have broken this into two sections. The first for embedding the component and next how to pass data back and forth.

1. Define a hook / mounter that embeds the React application

View Gist

This gist demonstrates what the LiveView hook might look like. Note that there’s a destroyed function defined that cleans up the React application. Also, I use webpack chunks because it keeps my LiveView application JS small. You don’t need to do this, but you’d run the overhead of shipping your React component to every page, where it’s not needed.

2. Embed a DOM element in LiveView referencing the hook

<div id="space-editor-instance"
     phx-hook="SpaceLayoutEditor"
     phx-update="ignore"></div>

Well look at that, pretty simple! Because we’re leveraging LiveView hooks, all of the nitty gritty of when our hook is called is handled for us.

It is important to use phx-update="ignore", otherwise LiveView’s DOM management will be in conflict with the React DOM management. This leads to an important caveat though. If you do something like data-important-thing={@from_live_view}, then it will be available on first mount only! It would not get updated if the value changes, because the DOM updates are ignored.

You can embed anything

This approach isn’t very complex, and largely works due to LiveView’s hook system. It’s also not really React specific. You could embed any JS “component” using this same approach, although the details would vary a bit.

For example, I use Quill for my HTML WYSIWYG editor. I mount it almost the same way, but it’s not a React-based library.

Data Flow

In the previous example, I alluded to ignored DOM updates causing some challenges with passing data using the DOM. In theory, you could mount a DOM element that is managed by LiveView and is “watched” by your application for value changes. I don’t follow that approach, though.

Instead, I leverage the data-flow bindings available in LiveView hooks. Here’s the previous component with the data flow bindings included:

View gist

Receiving Data (LiveView to React)

For receiving data in the React app, I can simply leverage this.handleEvent in the LiveView hook. This will get triggered anytime the server pushes a message down. It will then “re-mount” the React component with new props.

React is smart about this. When it sees that the component is being mounted but already exists, it will change the props but not destroy the application. The updates become very cheap to do.

Pushing Data (React to LiveView)

For pushing data from the React app, I leverage pushEventTo. I found pushEvent would sometimes push to the wrong element (when using components). I’m not sure if that was an edge case I hit or standard operation, so I started using pushEventTo and being explicit about the element.

I define an interface going from the LiveView hook to the React application. I would strongly suggest avoiding pushEventTo directly in React because it blurs the lines between the different frameworks. This would remove the ability to embed the React component in a different context, due to the implicit LiveView dependency.

Not That Complex

This whole thing is not very complex in practice. At its foundations, this pattern is following the basics of LiveView, Channels (data flow), and React. But, it is definitely challenging at first. Once you establish the pattern and see it working, it becomes an easy integration.

Discipline comes into play here, then. If it’s easy, it is also easy to abuse it and start embedding React components everywhere. I avoid this and will implement things in LiveView where possible. It’s convenient for extremely complex problems that are solved by existing libraries, so I keep to that use case.

In fact, I’d say it’s more than convenient. You can ship crazy things really fast because of it. I’ve had ambitious ideas and then turned around a working prototype within 1-2 days. For example, we recently shipped a new CSV-based importer that is built on an open-source React library. It took maybe three days total?

The Book Plug

My book “Real-Time Phoenix: Build Highly Scalable Systems with Channels” is available at The Pragmatic Bookshelf. This book explores using Phoenix Channels, GenStage, and more to build real-time applications in Elixir.

It’s been almost two years, is it still relevant? Understanding the foundations of Channels and data flow make building the things in this blog post way easier to ship and diagnose.

Real-Time Phoenix by The Pragmatic Bookshelf
View other posts tagged: elixir