Phoenix LiveView Server & Client State

Let me preface this by saying I’m a huge fan of LiveView—this discussion is borne purely out of love for LiveView and respect to everyone that works on it. Using it is an amazing experience!

Frequently when I’m working on something new, I have major decision paralysis where LiveView is involved, and it often comes down to just the need to render something like a modal. Usually the thought process goes like this:

  1. Find a LiveView Modal example or use the mix phx.gen.live generator
  2. Remember that modals whose open/closed state as in the generator and most blog examples result in what I consider to be sub-optimal UX (In poor network conditions or even when a random network blip occurs, what users typically expect to be an instant interaction suddenly has noticeable and frustrating latency associated with it)
  3. Find examples that use LiveView but show/hide the modal using JavaScript
  4. Realize the only JS inter-op that works well so far (that I’ve found) is Alpine.js, but using Alpine.js has security implications I’m uncomfortable with (I want to avoid a CSP with script-src 'unsafe-eval', which Alpine.js requires)
  5. Worry about having this debate for every component and wonder whether to just avoid the issue altogether and step back into the more familiar territory (for me) of a Phoenix API + React via Next.js, for example.

Even if CSP weren’t a concern, using a client-side library like Alpine.js still comes with a set of problems I’m not sure how to solve. Consider the following flow:

  1. User shows a modal containing a LiveView-rendered form via Alpine.js
  2. User submits the form, but errors come back and are rendered via LiveView
  3. User changes their mind, closes the form via Alpine.js
  4. The app sends a WebSocket frame to tell LiveView to clear form state
  5. User changes their mind again, re-opens the form, but the “clear the form” message is still in transit, and the user sees their old errors, which then unexpectedly disappear when the “clear the form” message is finally processed and the modal contents re-rendered

Of course, this is a very specific and somewhat contrived example, but I think it illustrates the general theme that without careful consideration, LiveView can take you from dealing with the already-difficult problem of managing a single UI thread to dealing with a second UI thread for certain pieces of state.

The answer here just may be that LiveView is specifically not the right solution for UI elements like modals and other similar UI elements right now, but then having to build an app where some components work via LiveView like a single page with a form on it, and others require separate templating/state management/etc. on the client seems to add additional complexity that I would avoid with an architecture that has a single source of UI state.

I’m curious if others have thought about this, and especially if you’ve got an app in production or development using LiveView heavily, how are you dealing with it?

5 Likes

Mostly, we just do the regular live view flow, and it works fine, even all the way across the world. Our servers are located in US east coast, and pings from Singapore are a little slow, but even still it isn’t actually slow enough to actually influence people’s workflows. (Caveat, we are B2B software not B2C, which may be more sensitive to such things).

Beyond the “don’t worry about it” response though, my company’s particular architecture allows for a very interesting model I’m hoping to POC soon: Geographically distributed live view servers. At CargoSense, our LiveView nodes are do not touch the database, rather they make GraphQL calls back to our platform nodes when they need data. This means that we could be distributing our UI layer nodes all over the world and then UI interactions that merely open a modal or don’t otherwise hit the DB just have to do a loop through the nearest liveview node instead of travel all the way to us-east4.

4 Likes

Are you planning to do this with epmd?

Curious to hear. If I was using gql in the liveview nodes my first thought would just be to call the gql through http and not have to deal with the complexity of node meshing.

Hopefully v3 of Alpine will address the CSP/security concerns (and maybe more?), as mentioned here.

3 Likes

Longtime lurker (pretty sure I have another account which I couldn’t find).

I feel your pain.

I was originally using alpine for the modal from the following guides:
Creating LiveView Modals with Tailwind CSS and AlpineJS
Create a reusable modal with LiveView Component - Tutorials and screencasts for Elixir, Phoenix and LiveView

I got it working, but it felt a bit messy. I finally settled on storing all the modal open/close state in a checkbox on the client-side and everything else on the server-side. No need for any client side js.

Wrote it up in more detail for posterity here:
No Javascript Clientside Phoenix Live View Modals

2 Likes

I finally settled on storing all the modal open/close state in a checkbox on the client-side and everything else on the server-side.

Are you clearing the form synchronously on the client somehow if the modal is closed? This is one of the race conditions I was meaning to point out in the original post, but may not have been clear.

If the modal has server state, and is closed and opened again, I think users would expect clean state in the modal, but they may still have leftover server state from before it was originally closed (and worse, the state may flash from the old state to clean state if you went a message on close to clear the form).

Ah, for my use case, I’m using the modal to set preferences for the rest of the page and then having the affected components update based on the change. So if someone opens the setting modal, their expectation is that the correct settings are there.

For your use case, what would need to be done is an event be sent when the form is closed to clear the state. For instance, you can do this in the “away hook” example I posted.

And, just my opinion, as your user expectations may vary for your site, but I wouldn’t want my input to be wiped out on a form if I closed it for whatever reason. I would expect it to retain my data, so I can continue where I left off. Having it wiped on a socket disconnect or whatever it may be would be super frustrating.

The answer here just may be that LiveView is specifically not the right solution for UI elements like modals and other similar UI elements right now, but then having to build an app where some components work via LiveView like a single page with a form on it, and others require separate templating/state management/etc. on the client seems to add additional complexity that I would avoid with an architecture that has a single source of UI state.

Have you thought more about this? I tend to agree.

I think the best use case for LiveView is:

  • you have a UI that is best generated server side (for example a table of users)
  • there is some action that can be performed and modifies/mutates this data (i.e. bulk delete)
  • you want the UI to reflect these changes immediately without reloading the page

In other situations where the interaction is entirely client-side (modals, dropdowns, hide/show via checkbox etc.), then I think JS is still the best tool for the job. Personally I like Stimulus but there are other options as well such as AlpineJS.

I do have to say that a lot of these components are not that straightforward… this is a good overview of what functionality is needed for a proper modal (focus trap, rendering in portal, scroll locking, etc.):

Then finally there are situations where both applies. For example a modal that contains a form but when you submit it should close the modal and refresh the data in the background (such as in your example I think?)/

Those are the especially tricky. At that point it is also worth evaluating whether such complex interactions are necessary or if a page reload or form on a different page would also suffice.

You can do a lot of that today with Phoenix.LiveView.JS, which can work purely clientside and even allows building optimistic UI for things, which do interact with the server.

Yes I know:) That only gets you so far though (and I have a feeling this gets overlooked).

For example:

  • I may want to open a modal in a portal so that (Headless UI – Unstyled, fully accessible UI components) so that it doesn’t interfere with other elements on the page with a z-index.
  • Or I may want to calculate the placement of a dropdown based on the viewport so it doesn’t fall outside of it (https://popper.js.org/).
  • Or make sure when the user tabs in a modal it does not active an input that is on the page behind the modal.

Furthermore, can you use LiveView.JS on a regular (dead) phoenix page? That would be nice. I tried phx-disable-with and it didn’t seem to do anything.

This is for sure a naive implementation, but still focus trapping in a hook: Add focus trapping and add aria tags to modal by LostKobrakai · Pull Request #3894 · phoenixframework/phoenix · GitHub

I don’t see a reason why poppler couldn’t be driven by Phoenix.LiveView.JS.

The portal usecase is for sure something not easily done without LiveView/morphdom supporting it.

No you can’t, but also it’s becoming less and less needed to do non-live pages in the first place.

1 Like

This may sound like flame bait but I would say don’t do modal. draw a new screen, your users would be happier. It is more intuitive, easier to implement, and no slower than a modal regardless if you are doing SPA style client side UI or LV style server side UI.

1 Like

No worries @derek-zhou :smile: I tend to agree.

This is for sure a naive implementation, but still focus trapping in a hook: Add focus trapping and add aria tags to modal by LostKobrakai · Pull Request #3894 · phoenixframework/phoenix · GitHub

That is pretty neat. For me this might be an indication that the main benefit of LiveView is lost because a fair amount of JS is still needed via a hook to make it work completely and you now have two technologies to think about (liveview + js) :thinking:

At some point it becomes a question of “can you” vs “should you”. And personal preferences definitely play a role here I think. I have a feeling for many people the appeal of LiveView is not needing to write JS and not needing to worry about the frontend so much. But whichever way you turn it, frontend is complex and good UIs are hard.

Are you sure about that? I prefer dead views for most of the app still because it is nice and straightforward and there is not much new to learn when coming from another MVC framework such as Rails. But perhaps this is the trend Phoenix is moving towards (everything is a live view?).

I’m not sure this is a good take. If the expectation is client side functionality then it needs to be in js (or wasm, …). I see the benefit in Phoenix.LiveView.JS on a similar level as e.g. stimulus. Driving client level functionality, which is implemented somewhere in the js. Glueing together JS functionality with where it’s meant to be applied.

There’s for sure a place where you have so much client side only interactivity that it no longer makes sense to have LV involved, but I rarely work in that area.

I also prefer the simplicity of non live views, but with heex being part of pheonix_live_view anyways and it being usable in both it’s no longer a hard jump.

1 Like