Functions versus stateless components

I’m looking for some good practices or rules of thumb on when to go for stateless LiveComponents vs regular functions.

I know that LiveView is rather new, so the goal of this post to get some great discussion (similiar to what was posted in LiveView with complex layouts ). I feel these discussions can help shape the practices around LiveView.

This is part of the docs (Phoenix.LiveComponent — Phoenix LiveView v0.20.2) triggered me:

You should also avoid using components to provide abstract DOM components. As a guideline, a good LiveComponent encapsulates application concerns and not DOM functionality. For example, if you have a page that shows products for sale, you can encapsulate the rendering of each of those products in a component. This component may have many buttons and events within it. On the opposite side, do not write a component that is simply encapsulating generic DOM components.

If I’m reading this right, this is saying that you should use function for abstractions that are not part of your application, like a styled button or badge.
I can see why that first part would be a good guideline, what I don’t see is when you should go for a LiveComponent then. Why pick a component instead of function (maybe a function in a module) to
encapsulate your application concerns. What is the advantage?

defmodule ProductComponent do
	use Phoenix.LiveComponent

	def render(assigns) do
		~L"""

		"""
	end
end

defmodule ProductModule do
	def custom_render(price, title, description) do
		assigns = %{price: price, title: title, description: description}
		~L"""

		"""
	end
end

Why would you choose ProductComponent over ProductModule?

I cannot think about any good reasons so far, but then I read this (Feature suggestion: phx-update="list" · Issue #1214 · phoenixframework/phoenix_live_view · GitHub) by @josevalim

Something else you could do and it should speed up things today is to continue rendering it as a list but render each list entry as a component. The components are tracked individually which means re-rendering the list is fast (we only send the component IDs).

So this means that picking components over functions could result in performance improvements. But that was not something I was expecting. On the other hand, it seems like it’s the exact opposite of the original guideline (don’t use components to abstract DOM functionality). Here I would have picked a function to render list items for the exact reason of not abstracting DOM functionality, but it would result in slower performance.

Lastely one related question to the above.

From the same docs:

Therefore it is your responsibility to keep only the assigns necessary
in each component. For example, avoid passing all of LiveView components
when rendering a component:
<%= live_component @socket, MyComponent, assigns %>
Instead pass only the keys that you need:
<%= live_component @socket, MyComponent, user: @user, org: @org %>
Luckily, because LiveViews and LiveComponents are in the same process,
they share the same data structures. For example, in the code above,
the view and the component will share the same copies of the @user
and @org assigns.

but if the components share the data structures, then what is the difference between passing all assigns and just some of them?

2 Likes

Your example of component vs. function doesn’t show a clear reason for using the component simply because you shouldn’t use components, which just renders some markup. That’s exactly what the documentations is telling you or at least trying to. Components are for encapsulating markup and their related event handling. Your example component is missing the second part.

This is completely unrelated to the component vs. function discussion, which more of a server-side and also code organisational concern.

What jose is suggesting is about client side performance. On the client liveview is using morphdom to apply diffs. Morphdom does this by comparing the current dom with what is supposed to be shown and doing the relevant updates to morph the current dom to whatever it’s supposed to become. This diffing can become expensive especially for large dom sections usually generated by large lists of things. Now nested liveviews as well as nested components in liveviews basically break up the boundaries of what morphdom needs to diff.

If only a component changed then everything outside it’s bounds can be ignored.

This is different to an button rendered by some function having changed. Here the server as well as the application by morphdom will still need to evaluate the complete tree of markup for the complete liveview or component that button is part of. It can still be a small diff, but there’s no smaller boundary than the liveview or component to apply the diff by.

The situations I can see where creating an component for the list items might be good for performance are also cases where it makes sense to have a component for the event handling as well. There might still be cases where it might make sense to convert a function rendering markup to an component without it also having event handling, but I’d expect them to very much be the exception.

1 Like

So is the rule as simple as that? With event handling => use components. Otherwise use functions?

Thanks, that’s clear!

1 Like

There might be some other concerns as well, like e.g. the preloading callback, but generally, if all your component does is render assigns as it gets it, it can just as much be a function.

I think part of the issue here is that the term components is ambiguous in this discussion. Almost all of the snippets that you have mentioned are actually about stateful components, the first snippet is linked inside the “cost of statefull componets” and the GitHub comment mentions mention components IDs, which are for tracking stateful ones. In any case, I will change those to be clearer. :slight_smile:

Between stateless components and functions, there are very few actual differences, so I would even say they are largely interchangeable.

2 Likes

As I understand it, stateful components run their own process, so there is some overhead.

But do stateless components? Or are they “compiled down” into the final template render?

Also my understanding is that, LV cant track inside functions, so this must be re-rendered in total:

def product_card(name, stock) do
  ~L"""
   ...
   name
   ...
   stock
  """
end

My understanding is if the stock changes on a product, the entire card is re-rendered? Is a stateless component able to track more accurately?

def ProductComponent do
  def render(assigns) do
   ~L"""
    ...
    @name
    ...
    @stock
   """
end

Would changing stock here only update that specific element or would the whole component be rendered again?

They do not. They still run as part of their parent liveview. Only liveviews have their own processes.

1 Like

Hmm, so where does the additional “cost” of too many components come from?

There’s still overhead of handling the state of stateful components even if not within its own process.