What strategies are you currently applying to reduce the amount of code inside a LiveView?

For the past two to three weeks I’ve been working with LiveView in a side-project and one thing that I’m noticing so far is that my LiveView code is getting “bloated” very quickly.
Since I’m working with one LiveView as the single source of truth and a lot of LiveComponents which are constantly pushing state updates, I’m handling all of this information flow in a single place - the LiveView.

That being said, I was wondering what strategies are being used by the community to improve this…

One of the things that I was thinking is: creating a live/handlers folder and place modules that will concentrate the logic that manipulates the information there; letting the LiveView code exclusively to receive the events and call those modules, akin to the “thin controllers & fat model” approach. Has anyone tried something like this?

2 Likes

For me it was building reusable components. It’s a bit harder to design such a component to be that flexible but in the end it helped me a lot reducing code. It’s also simpler to test and maintain. At least for the Frontend there is a WIP library Surface which tackles the problem.

One other thing is to split the concerns. The UI related code is handled in each component while the parent LV is handling only the relevant information of the application. And there you can also split the callback algorithms (logic) in discrete Modules which you can reuse in other Projects.

Hope I could help.

1 Like

What I’ve been doing is that my LiveComponents are the source of truth all of them just receiving current_user as the obligatory assign. This way my live view only cares about the “Page behaviour”, and my components about their internal interactions.

What I have in mind in case I need to send and receive messages to the components, is to make a small macro which injects something like:

# On the live view
use ComponentName.MacroName

# What the macro defines
def handle_info({ComponentName, message}, socket) do
  send_update(ComponentName, message)
end

I created live_props to help simplify certain parts of my LiveViews/Components.

It does not really provide any conveniences for Component -> LiveView communication, but it does provided conveniences for defining default values and re-computing assigns which can reduce the amount of code you have to write.

When I started to break up a monolithic LV into smaller components, I also decided to use the LV as the source of truth, but I opted to handle any events in the relevant component themselves.

So originally I had a lot of handlers in the LV for CRUD for 3-4 different resources. After the refactor I had 3-4 components each handling the CRUD (usually just updates and deletes) and notifying the LV when those updates happened using PubSub, and then LV just needed to handle list refreshing.

I also noticed that the components were using a lot of the same ‘utility’ functions in the LV, so I moved a lot of those into helpers I could import, which also greatly reduced the code in the LV.

Hey @Menkir! I heard about Surface before, but haven’t tried using it yet. I’ll keep exploring LV until I seed the need to use another abstraction on top of it.

How are you making the distinction between “UI related code” and “relevant information of the application” in your LiveViews? For my scenario, there’s constant communication between the LV and the LiveComponents which compose the LV state. So, everything inside the LV is already part of the UI and the “relevant” portion is handled by application contexts.

What are you defining as “page behavior” for your application? Thanks for the reply!

Hi @bluejay! I haven’t heard of your lib before, I’m definitely gonna check it out. Are you using it in production? If so, what major gains have you noticed so far?

@tfwright I’m also doing the same thing as you - all of the events are handled inside the components, but since I have to keep the main LiveView up-to-date when changes happen, I also have to handle a lot of events in the LV itself, mainly regarding the LV state (idk, perhaps I should break things in more components).

1 Like

Personally I am not so concerned with large files necessarily, as long as each code block is short and easy to understand. I made the decision to refactor the LV into smaller components when I noticed “clumps” of related logic pertaining to a specific, isolated piece of UI. There is maybe one more component I could add and then I would have 2-3 event handlers left that are truly of “global” concern to the view. The rest are params handlers (I am currently storing a lot of state in the URL) and info handlers that mainly just trigger db queries or set assigns and therefore add very little complexity to reason about:

  @impl true
  def handle_info({:download_complete}, socket), do:
    {:noreply, socket |> assign(loading: false)}

  @impl true
  def handle_info({:deleted_annotation}, socket),
    do: {:noreply, reload_source(socket) |> request_reanalysis}

Etc

For instance: The Single Source of Truth LV doesn’t care about e.g. if a modal is open or not because this behaviour is stateless (most of the time). These informations are completely handled inside the components. The Leex gives you a simple way to dispatch events according to their concerns.

Ui related code can simply be identified if you use a ui framework. There are well defined parameters which you can store and handle in a component. Less bloating callbacks inside your parent LV :smiley:

Yes, using it in production and the major gains from my standpoint are as follows (for simplicity I am using the word module to refer to LiveViews and LiveComponents)

  1. By declaring all my assigns (as props or state) at the top of my module, I can see at a glance what data the module needs to function. I also have an easy place to look when I can’t quite remember what I named a particular assign)
  2. By declaring default/initial values, I can greatly simplify my mount/update callbacks for modules that have a lot of state (or even bypass them entirely)
  3. Props are automatically added to a LiveComponent’s module documentation, so I can easily see what props a LiveComponent expects without navigating to and opening the file.
  4. Setting props as :required will raise errors if those props are not passed in to a LiveComponent, which helps spot errors more quickly
  5. If i have “computed props” inside a LiveComponent, those will be refreshed whenever the props are updated. For instance, the following snippet is for a LiveComponent that expects as :user_id to be passed in, which we use to fetch the user from the database. Anytime the :user_id changes, :user will be refreshed. Again, this saves me from having to write my own update callbacks.
use Phoenix.LiveComponent
use LiveProps.LiveComponent

prop :user_id, :integer, required: true
prop :user, :struct, compute: :get_user! 

def get_user!(assigns) do
  Repo.get!(User, assigns.user_id)
end
  
 # ... rest of LiveComponent
1 Like