timadevelop

timadevelop

So my experience with LiveView has been good so far but I always come back to this single issue I cannot figure out a good solution for: persistent client-side components and navigation.

I frequently need to introduce Svelte or React components to my apps because otherwise I’d need to spend irrational amounts of time to build something like a map component or a complex form with animations. One such component is a dashboard sidebar that handles user info, navigation, switching between accounts / workspaces, etc. with lots of nested menus, dropdowns, mobile support, etc.

Let’s say I have a SaaS product.

  • on /home, /about we render some basic landing HTML without liveview
  • on /dash, /dash/X, /dash/Y /dash/Y/:param, /dash/Z we render complex LiveViews with many subcomponents and view-specific events.

For all “dash” routes except /dash/Z, I need a complex sidebar. I pull a 3rd party react component that handles all the logic and keeps local state in React Context or useState or a combination of these. There is a client-side state about things like opened dropdowns inside the sidebar, text fields user is editing, emoji icon selector, etc. Sidebar obviously has links to other pages in the dashboard (e.g. from /dash/X to /dash/Y). I wrap it into my own React component to accept props and push events within a LiveView (e.g. serialized current_user, pushEvent("save_username")).

Now we need to figure out where to mount this component so it doesn’t loose client-side state on navigation between LiveViews. Here are the options I found so far:

  1. The trivial approach would be mounting wrapper React component inside each LiveView. This has some downsides though. If user navigates from /dash/X to /dash/Y, Phoenix changes LiveView, which leads to loosing client side state stored in React Context. To solve this, we need to pluck state management out of the React Context into something global. Sure thing it’s possible, but it would require reworking 3rd party components, which kinda defeats the objective and will lead to complex logic if component is used multiple times on the same page. Even if we rework the components to use global state, we still need to deal with potential flickering because components are re-mounted on live navigation. Note that LiveBeats approach with LiveBeatsWeb.Nav is not related to this issue, but it works almost as described here afaiu. If you go https://livebeats.fly.dev/, open developer tools, disable cache, and navigate between “settings” and “my songs”, you’ll see that profile image “flickers” / gets reloaded after each navigation.

  2. Use single LiveView across dashboard for all routes and implement navigation within single LiveView using sub components. This was discussed here Structuring a live view project with proper navigation - #2 by chautelly, but I didn’t explore this route because I believe it will become insanely hard to maintain after more routes are added (users, orders, posts, items, etc.).

  3. Render Sidebar as separate LiveView in root.html.heex with something like

<%= if assigns[:has_navigation] do %>
   <%= live_render(@conn, App.SidebarLive, session: %{"user_id" => assigns[:current_user].id, "current_page_id" => assigns[:current_page_id]}) %>
<% end %>

with :current_page_id assigned by the “content” liveviews we get from router. It works on first render, but we need to update it… As we know, LiveView doesn’t allow to update root layouts from LiveViews by design (Live layouts — Phoenix LiveView v1.2.5), so this won’t work.

  1. Render separate LiveView in root layout and use PubSub to share specific assigns between 2 LiveViews (SidebarLive and the one that is rendered from router)
<%= live_render(@conn, App.SidebarLive, session: %{"initial_user_id" => assigns[:current_user].id, "initial_current_page_id" => assigns[:current_page_id]}) %>

This would work. It seems similar approach was discussed here What will happen to my LiveView component if I go to the next page? - #24 by chrismccord and here LiveView with complex layouts - #29 by reddy. But involving PubSub for UI changes worries me a lot for some reason. This just doesn’t feel right for a sidebar component. Maybe I’m wrong and it’s OK to utilize PubSub for this?

  1. Embrace client-side state instead of PubSub - render separate LiveView like in option 4. but instead of using PubSub, attach to dom updates in JS in regular LiveViews and update Sidebar component using native DOM events / shared document-level objects. Example with events:
# app.html.heex
# note: we could use window event listener in JS instead of phx-mounted
<div class="hidden" phx-mounted={JS.dispatch("app:sidebar:update_page_id", detail: %{current_page_id: @current_page_id, has_navigation: @has_navigation})}>
# in react component or in plain javascript
useEffect(() => {
  const updatePageId = (info: any) => {
    // client-side change if sidebar server state doesn't care about this
    setCurrentPage(info.detail.current_page_id);
    // or server-side driven UI change
    // we  `pushEvent` to the server, which will update sidebar server
    // state and will trigger an update in react component **preserving**
    // client-side component state
    live.pushEvent("update_current_page_id", {
       page_id: info.detail.current_page_id,
       has_navigation: info.detail.has_navigation
    })
  }
  window.addEventListener("app:sidebar:update_page_id", updatePageId)
  return () => {
    window.removeEventListener("app:sidebar:update_page_id", updatePageId);
  }
}, [setCurrentPage]);

This approach moves assigns sharing logic between LiveViews from PubSub to the browser. This works and doesn’t require PubSub communication between LiveViews for UI. So instead of action->PubSub->effect, we have action->browser->effect.

Last 2 also have a couple of flaws:

  1. Each LiveView has it’s own process and the “Sidebar” LV is not mounted from router. So in order to request navigation to different state or even “put_flash”, we need to send messages over PubSub to our “regular” LiveViews. We can replace the PubSub for navigation and flash the same way with browser events, but navigation and put_flash logic belongs server-side so I’d rather keep it on server, in secure & reliable PubSub and not rely on client browser.
  2. We have to always keep that LiveView alive for each active session, even if sidebar is not needed on some pages.

Curious to hear how others approach this problem.

Maybe there is a better and simpler solution to this? Maybe it’s possible to do without using a separate LiveView?

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews