More convenience functions to deal with assigns inside LiveView

Hi everyone.

Some time ago when I was doing some heavy work with LiveView for a game I was developing (now paused) I noticed a pattern that I kept using over and over again in other projects to clean up code.

I’ve been meaning to push some contributions to LV for quite some time, but I don’t know what the protocol to accept changes like this are. So I’d like to know, specially from @chrismccord if this is something desirable to have in LiveView. Here’s some of the helpers I’ve been constantly reusing in projects:

  • An assign_new version that supports passing just a single value:

If you have lots of assigns in your LiveView, chances are that you have used this a lot before, so it might be useful to have another version that also allows you to pass values to it: assign_new(socket, :title, "Hello World") instead of the more verbose one that takes a function assign_new(socket, :title, fn -> "Hello World" end) and does the same.

  • An assign_merge function that takes a function or a value (a map) and merges it with the assigns that exist in the socket - we can also have an assign_new_merge counterpart.

Time and time again I saw myself using this helper because I had functions that returned the initial (or updated) values for the LiveView state and I had to reassign the values by hand every time. It works like this: assign_merge(socket, :state, %{title: "Hello World"}) or assign_merge(socket, :state, fn -> %{title: "Hello World"} end).

One might argue that the abstraction is too thin, since we have the Enum module functions, but I think it not only makes you write less, but it adds a lot of readability to your code, specially when you have lots of assigns in a pipeline.


PS.: On a side note: I always thought about the assigns functions like the ones we have on the Map or Keyword module like put, put_new and put_new_lazy but LV does not follow this convention (probably for some good reason). I don’t know if this was discussed before or if keeping “compatibility” was ever intended but if we were to have a breaking change, it should be considered before 1.0 (if anyone knows the answer for this, please share).

My guess around assign_new only accepting a function is that since there is likely no good reason to ever store a function as an assign, it’s more concise—especially for framework code—to just have the single higher order function. It’s also kind of self-documenting to say, “Hey, there’s no good reason to store a function in assigns!” Highly speculative, of course, since there’s nothing stopping you from using assign to store a function.

I don’t see a version of assign_new that took a non-function arg being accepted because now you have a function that looks like it just assigns stuff but gives functions special treatment. This means it would have to be more verbosely documented and there would probably still be a steady enough stream of people asking: “Why did assign_new(socket, :foo, fn -> "hello" end) store "hello" and not my function??” I believe that is called Principle of Most Surprise :wink:

Where are the places you are using assign_new a lot? I’ve only ever used it to store defaults which was made obsolete by attr. And defaults for live components can always be done in mount/1. I’m interested in your usecase regardless!

Good point! I see how this could be confusing if we have both options under assign_new. In that case, making the distinction by having different functions would be better (eg: assign_new_lazy), but now we are talking about introducing a breaking change to the API (still doable though).

There are still some cases where attr is not enough, especially when you are dealing with computed assigns (e.g.: if are doing stuff like class variance authority) or when you have more complex slots (IIR attrs in slots don’t support required and defaults yet).

For this game I was developing, I had fewer components that needed something like assign_new but I used assign_merge a lot… I had GenServers representing part of the game world and they were responsible for holding the game state (like player actions and such). Because LiveViews acted as “windows” into what was happening in the GenServer, they were always receiving communication back from those GenServers with new information that had to be merged back, usually in the shape of maps with many assigns.

Edit.: I thought about another use-case after posting, which is using assign_new on an LV mount to compute stuff just once… I’m not sure if I remember this right, but since LV is mounted twice, I guess I was using assign_new to process expensive stuff just once, so I was using that a lot too

about assign_merge

Currently assign/2 allows to pass a map or keyword list:

A keyword list or a map of assigns must be given as argument to be merged into existing assigns.

though, it doesn’t have a “lazy” counterpart that would take a function…

1 Like

That’s interesting, I never thought to do that. If the computation is still going when the socket connects, do you have to handle that scenario? Will it trigger a re-render in that case when it does complete? I’ve never developed a game so it’s that would be full of considerations I’ve never, uh, considered, ha. That case does seem like a good fit for the upcoming assign_async.

I’ve never had attr need anything more than a simple value. I’ll have to look up class variance authority as I have no idea what it is so definitely not in a position to comment!

The problem I guess is that since mount is called before and after the socket has connected, if you do just assign, you’ll be processing the value twice (going to the database two times for instance). I’m not sure if it’s the same use-case for assign_async, which seems more useful for slower computations (like you mentioned).

(For this game in particular, the computations are not usually too complex and don’t take longer than some ms, but I still have to do lots of queries to retrieve the state of game objects from the db, and some of those depend on others to be computed and so on).

As is typical of me I either neglected to express some thoughts or was careless while deleting sentences in an attempt to be less verbose. I was imaging using assign_async in conjunction with testing for connected?(socket).

This is unavoidable, and does not change by using assign new. The static connection and the live connection do not share values at all, they may not even happen on the same elixir server.

Where assign new helps is that in the case of the static render, you may have assigns already set from your plug pipeline. Assign new lets you reuse those on the static render, and then forces you to tell Phoenix how to fetch that same assign for the live render. This helps you fetch it only the 2 times and not 3 (twice in the static render, once in the live).

The only way to avoid fetching things twice is to use live redirect between pages so that you skip the static render entirely.

2 Likes

You can also do:

socket =
  if connected?(socket) do
    assign(socket, :foo, App.get_foo())
  else
    assign(socket, :foo, nil)
  end

no? Or are you making another point I am missing?

1 Like

Yes sorry, I was not as precise as I could be. The mount itself is unavoidable, but you can condition on connected to avoid loading stuff in the static mount. Mostly what I mean is that if you need a value on both the static and live renders then it will necessarily be fetched twice, assign new doesn’t change that.

1 Like

Right, yes, I think I introduced confusion by not directly asking if @thiagomajesk required it on static mount and sort of ran with “maybe” while trying to answer :upside_down_face:

1 Like

Not completely. It works for the case @benwilson512 explained. But there’s another usecase assign_new helps with, which you cannot split appart with connected?/1. If you use nested LVs and they’re connected then assign_new allows sharing assigns from the root LV to children, so e.g. a current user is only fetched once and not for each individual LV.

That sharing (and not refetching) is the reason assign_new needs a callback to be passed.

3 Likes

Oh interesting! I’ve never used nested LiveViews so that never occurred to me.

Did not know that, interesting :thinking:… Do you have any idea why it works like that?

I thought about using skeletons (placeholders) for the static mount of the page before, but it makes more sense now if we can’t avoid hitting the database twice on mount. It’s a good idea @sodapopcan, but I’d have to think more about in the future to see what are the downsides (for this particular use-case I mean).

By the way, it seems assign_async can potentially introduce something similar to an island architecture in the future if I understood it correctly, where things are fetched independently. For this scenario I presented might not be that useful, but I can think of others where it could be, that’s great :+1:.

Even if this is a little bit off the main point, this was fun, thanks :blush:.

This is one of the examples that came to mind, but I’d love to see if we have other helpful patterns in the community as well. I personally think that it would be great if we had some room to create other helpers that allow us to write more concise code in LVs for cases like the ones I mentioned.

1 Like

They’re just straight up different connections from the client browser. When the browser first goes to say http://example.com/page it does an HTTP GET request. To respond to this GET request Phoenix does the static render to return static HTML. The static render is done at this point, and the Elixir process that handled it has come and gone. The HTML contains the CSS and Javascript links, which the browser also then loads and runs (assuming the browser is setup to run javascript).

The Javascript includes your Phoenix LiveViewJS code, which then connects and opens a websocket connection to the server. It’s then over this websocket connection that the second render occurs to establish the actual connected live socket.

2 Likes

That’s true, completely forgot there was an HTTP request first :melting_face: