Livex: A working DX Experiment in potential LiveView enhancements - Could we see some of these features added to LiveView?

LiveView is by far my favorite web tech, but a few things have been nagging me. So with all the fancy and ill advised elixir tricks I could muster I got LiveView to do some new things:

tl;dr - Automatic client side state management (including for components), a tightened up render → reducer cycle, and custom events makes for a rather different, more functional React like programming experience. Gets rid of most uses of mount, handle_params, handle_event and push_patch in favor of a more declarative style.

Not production code, but an experiment in what if we had an interesting combination of new features? Would it make a difference to us? Could we see some of these features eventually added to LiveView, or at least implementable with less high-wire activity than I had to use.

34 Likes

This is some impressive work which at a glance seems highly useful! I see how this would help (React) developers to make the step to LiveView.

Greatly appreciated :slight_smile:

2 Likes

This looks really interesting. EmberJS does something similar with query parameters where if you assign to them it updates the URL which is convenient.

I’m definitely gonna give this a little spin!

2 Likes

Nice additions! I really like the idea that assigns can have dependencies.

Maybe the function provided to assign_new/4 could take the list of dependent assigns as a second argument. And assign_new could take a single atom and single extra argument in the callback.

socket
|> assign_new(:new, [:user_id], fn assigns, [user_id] ->
  ...
end)
1 Like

Lovely

EDIT: I’m looking at the API and this is very lovely

1 Like

Thank you for this detailed and explorable contribution !

This sunday evening I continued an exploration of the space of derived/computed properties too in a little poc, but outside of Liveview. I tried to extract dependencies by inspecting the pattern match in the computed function head. I hate magic but I’ve found this little sprinkle to be useful.

computer "Pace computer" do
 .... input definitions .... 

  val("pace",
    description: "Your running pace in minutes per km",
    type: :number,
    fun: fn %{"time" => time, "distance" => distance} -> time / distance end,
  )
end

The previous version needed :

val("pace", 
    description: "Your running pace in minutes per km",
    type: :number,
    fun: fn %{"time" => time, "distance" => distance} -> time / distance end,
    depends_on: ~w(time distance))

But I’ve found this to be redundant since the fun already declares its dependencies by pattern matching on values. Being a compile-time extraction that looks at the anonymous function head’s AST, this still is runtime-free for tracing dependencies. Note that I’m not well versed at all at macros so don’t take my code for reference.

Maybe that kind of pattern could be useful for ergonomics ? In my poc I chose to enforce it and will soon raise if there’s no pattern match.

This is looking really good! Thanks for creating and sharing.

A few thoughts:

  • I like your idea of assigning initial state in the state macro even though it’s not there yet.
  • I wonder if assign_new should be named assign_derived or something similar for the derived state.
  • I think it’d be nice if it wasn’t necessary to explicitly declare the dependencies and if anything with assigns. would cause a re-run. Similar to how svelte makes it an opt-out rather than an opt-in.
  • I think the JSX features should hang directly off of JS to avoid confusion.

Or have a different name to avoid conflicting because JSX sounds like React’s JSX and it’s not it

1 Like

What extra does that get you? You’ll already get the updated user_id on assigns so you can say assigns.user_id within the anonymous function.

My goal was to try and stay LiveViewish, so given we have change tracking already it made sense to build on top of that to keep one idea about what a change is.

Interesting approach though.

1 Like

I wonder if assign_new should be named assign_derived or something similar for the derived state.

Yeh, I like the explicitness of that. I went this way because I thought there was some elegance in using assign_new because the existing assign_new does work like a special case where it has zero dependencies (and so is only populated if it doesn’t already exist).

  • I think it’d be nice if it wasn’t necessary to explicitly declare the dependencies and if anything with assigns.
    would cause a re-run. Similar to how svelte makes it an opt-out rather than an opt-in.

Interesting idea, and probably even possible. But not that easy. And for a pretty simple addition (it’s only about 10-15 lines of code) this version provides a nice usability improvement for relatively little effort.

  • I think the JSX features should hang directly off of JS to avoid confusion.

Yeh, this is just an artifact of this being a (somewhat cludgey) wrapper around liveview rather than a set of patches for liveview. If I was contributing patches, I would call it JS. The main goal here is to be able to try out a set of API improvements together to better evaluate whether they’re worth including upstream.

1 Like

Looks tempting. Just make sure the streamlined url params part is not too limiting (not saying it is - haven’t had time to look deeper into your code yet).

To give you an example..

In the frontend app I’ve been developing in LiveView, we ensure the url query contains a meta attribute on where from in the page scroll container (the data “page” + item id in it) the user navigated elsewhere so when they press the back button they get returned to the exact same position in the (infinite) scroll. And this thing is nested (components have streams requiring this while the items can open modals, sometimes in a child LiveView, which then can also have their own streams which open other components). This nesting needs to be present in the url meta attribute so the page can fully reconstruct with everything positioned as it was prior to navigating away or even if they hit the refresh button.

How do you do it now? Do you use push_patch, or do you update the url with a hook?

Just wanted to chime in and say this looks great. FWIW there’s been a push in the frontend world to move client state into the url as well. I feel like this has been the one justified criticism of LiveView in the past (not all state needs to live on the server and incur a roundtrip latency cost). Having a declarative way of managing it would open the door for alot of use cases where folks would otherwise reach for a spa.

3 Likes

Can’t use push_patch only to replace the url for it would add an additional client-server roundtrip, so what I do is have a JS.dispatch( "replace-state") followed by (piped with) whatever JS function I need for the patching/navigation and then I have the replace-state listener in AlpineJS perform the url replacement before the actual navigation takes place, i.e.:

x-on:replace-state="window.history.replaceState( null, '', $event.detail.path)"

I’m not using hooks much (apart from some very generic ones to cope with issues of the LiveView/AlpineJS integration).

Makes sense. Yeh, the current implementation takes over the whole url and rewrites it.

It’s down the road a bit (I think my first attempts to contribute upstream will be the assign_new/pre-render enhancements) but I think there’s a range of interesting topics related to this. i.e. there’s pure client side data, but also perhaps assigns that are optimistically done on the front end and eventually synchronized with the backend

1 Like

out of curiosity, has this been proposed/discussed with LV core team or you’re planning on doing that once you have that proof of concept in the shape you like it to be before presenting?

In the first instance it’s for me (and any other interested person) to discover whether putting some of these improvements together meaningfully improves the DX.

I’ll be submitting the first small PR soon (assign_new with deps), and so I’ll learn more about whether there’s any enthusiasm from the maintainers for this sort of direction.

5 Likes

In general, I think you should aim for flexibility in future. Taking just a url is hardly sufficient in many scenarios when somethings needs to be done immediately before or after client-wise (hence the JS piping).

I described another UC here (unfortunately, still an open issue): The inability to differentiate b/w transition when item is removed and transition when its container is removed · Issue #3199 · phoenixframework/phoenix_live_view · GitHub, specifically the third option (the “hack”).

This sort of scenario makes my head hurt a little bit. But I wonder if this helps:

The way I implemented client state is that the top level div in the liveview and each component contains attributes capturing the state. And then we update the browser history url and liveview’s internal patch url based on a dom search for that state.

And so this does open up the possibility that you could manipulate assigns on the client optimistically and then synchronize it back to the server.

I can’t really get my head around whether this would help in the scenario you describe though.

1 Like