Phoenix LiveView: How would you build forum (Discourse-like) in it?

I have been looking at the awesome LiveView (awesome job @chrismccord!) and especially the examples, trying to figure out how I would apply this to real life projects I work on, especially the big and complicated UI-wise.

As many others here probably do, we are heavily using React.js, and often pairing it with GraphQL + Apollo at the moment, especially GraphQL subscriptions which come in handy in building real-time live UIs.

The more complicated projects are often similar to what Discourse UI does (this forum), rather than relatively simple single component like thermostat, counters or even user edit forms from the example repo: https://github.com/chrismccord/phoenix_live_view_example

So, just like on this page, we may have a header with top-right avatar of user. The avatar would typically display a notifications icon (or none), possibly with count of unread notifications. You click on the avatar and menu with notifications and/or links to profile edit pages appears.

In the main area of the page there some sort of main component, letā€™s say itā€™s a list of things, like a thread on forum. The items can be added/updated by other users, and we have live updates to these items. If itā€™s a forum, this can be a thread of hundreds posts, all fairly long and displayed on the same page.

Then you have some more live, interactive things like ā€œUser is typingā€¦ā€ or ā€œUser is editing this messageā€¦ā€ appearing where you would expect new message to pop up. When you hit ā€œEditā€ on a post, a modal or in-line editor appears allowing you to edit some content, using @ to mention other users.

Thatā€™s roughly the level of complexity a real-life apps have to deal with, and I wonder how - if at all - Phoenix LiveView is going to help me build these kinds of apps.

With React, or really any other client-side framework, we heavily rely on modularization using components to isolate independent or semi-independent things in the interface. The top-right avatar would be one, listening to changes on user notifications count and reacting accordingly. The list of posts would be another one. Individual posts would be own components etc., helping developer to build UI that is still coupled at runtime but has the logic very much scoped into individual elements.

With LiveView, if I understand correctly, this is yet not possible and when - letā€™s have example - a top right notifications count updates, the whole page including header, list of posts and open edit boxes would be re-rendered and merged into usersā€™ DOM by stealth. The same would happen with ā€œUser is replyingā€¦ā€ changes, the whole page including the header, list of posts etc. would be re-rendered server-side and sent down the wire to client for updates.

My main question is: do you think we should build apps like that (now or in future) using LiveView, and if yes - do we need a pattern of components to help us out - or the current approach of re-rendering everything and sending update down the wire to client will be enough?

12 Likes

Iā€™m really wondering if you actually need to wrap the whole page in a live view. I see all the interactions youā€™ve named as quite self contained and localized to some small part of the dom/website and not really one, where a change does actually need to affect the page as a whole. Like even the list of notifications and the counter could be separate as rerendering an additional ā€œnumberā€ is not really a big commitment besides the obvious need of rerendering the notification list (if open).

Iirc gitlab uses vue in a similar fashion, where the root vue instances are mounted to just some dom node where needed, while the bulk of the page is dom not managed by vue. Svelte does also follow a similar philosophy. Just make the small chunks in a page interactive, which need the interactivity.

2 Likes

Iā€™ve been thinking the same, as I tried to ā€œcoupleā€ the counter example with the embedded ā€œimage editorā€ eg when you increased count I wanted the image size to increase as wellā€¦ which utterly failedā€¦

Think we are looking at a PubSub solution.

eg. this forum the top right notification will be a component with a PubSub on the user eg topic ā€œuser_Xā€ - and any notifications there will go to all the users sessions.

then we have the loaded threads list - which would be a PubSub on the ā€œthreadsā€ eg topic ā€œall_threadsā€ and then that could update with the ā€œ3 new topics - click to loadā€

when you open a thread that would be pubsubbed to ā€œthread_Xā€, and would be able to show the ā€œreplying toā€¦ā€ and show new posts just made etc.

the post form would then go on the session(socket?) which by default runs in the session(socket?) and would not require pubsub.


a shopping cart is also an interesting example to exploreā€¦

All caveats apply - absolutely just guessing - there is also something about parent/child in the liveview which I assume can be used to send ā€œeventsā€ to each other to update stateā€¦ but not sure how to interface that and if such coupling is advisableā€¦

Agreed on the PubSub approach. There is an example of this in the Users example :slight_smile:

Try the demo at https://phoenix-live-view-example.gigalixirapp.com/users

You can change the username in another browser window and the username is automatically updated in the index page using PubSub.

The relevant source files are
https://github.com/chrismccord/phoenix_live_view_example/blob/master/lib/demo/accounts/accounts.ex and https://github.com/chrismccord/phoenix_live_view_example/blob/master/lib/demo_web/live/user_live/index.ex

6 Likes

The live updates across multiple tabs/browser windows but still retaining one, single, top-level component that renders contents of each page is one thing, but breaking the components down on a single page is another, semi-related issue. Iā€™m not sure if the later is supported or recommended way of doing things. I suspect things can get hairy very soon as you start adding/removing these dynamically on the page, for example, but most importantly - I donā€™t even know if itā€™s possible/doable/planned.

Dynamically rendering child LiveViews is already supported and you can add/remove them at will. For the ā€œbreaking into components and updating a single ā€˜pageā€™ā€ usecase, you would use the local registry with messaging passing, and everything should work as expected.

With a local registry, this is as easy as a message to the child from the parent :slight_smile:

wrt to discord (chat) like functionality, I think we can get there, but thereā€™s still a couple big things we need to solve, which are on the roadmap, but TBD.

  1. We need append/prepend operations on collections, as today you are right we have to re-render the collection to insert a new item
  2. We need a way to ā€œdiscardā€ state on the server after weā€™ve pushed it to the client, so you can avoid holding collections in memory when you donā€™t need to operate on them

This is a misconception, as we donā€™t re-render everything on the server (or client). The server does change tracking, so your render/1 function only executes the code with assigns that have changed. Otherwise itā€™s an if check and noop. On the client, we compute the HTML from the full local diff, but we only re-render each child LiveView (component), so your header example would only have the client re-compute the header.

Our goal is to find where the model breaks down, but I think we can get to what youā€™re shooting for. Still some work to do for that to happen efficiently, so stay tuned :slight_smile:

19 Likes

The point 2) on memory management and being able to discard something is very important for large collections, I think. For something like forum post thread, thereā€™s no need to keep individual Post on hand until it actually changes, then needs to be re-fetched.

When you mention dynamically adding or removing live views, do you mean it would be similar to React where you have a parent live view rendering a list, and then for each element in collection have a child live view rendering singular element?

Just an honest question - is it possible that your notion of a component is preventing you from getting the most out of LiveView?

For example in Elm components are seen as counterproductive (Components in Elm: why (not) and how?).

Now in React the term component is ubiquitous but I think the role of components in React deserves some closer scrutiny.

At the most basic level is the functional component. It transforms data in the form of props into markup. More recently through hooks it can also access state and effects but this loss of purity is a choice (optimization?) of the platform. A functional component could have just as easily accepted oldState as a parameter beside the props and returned newState alongside the rendered React Elements. Still the fundamental idea is to take a minimal set of relevant data and transform it into the necessary markup.

Class components have always had access to state - but even then not in the conventional OO sense because React is managing the state on behalf of the component instance. So Reactā€™s ā€œcomponent stateā€ is actually a separate data structure managed externally from the component instance. With class components you get into a conflation of representation (the markup) and behaviour. Now some of that behaviour is tightly coupled to the representation but it is all too easy for other functionality to slip into that component.

In 2015 Dan Abramov wrote Presentational and Container Components - as a way to manage complexity in larger scale projects:

  • Presentational components are ā€œdumbā€ i.e. they know how to turn data into markup
  • Container components are ā€œsmartā€ - they typically do not have a visual representation but exist primarily to coordinate the flow of data for the presentational components they manage. They may also furnish event handlers to be injected into their presentational components.

So both of these are ā€œcomponentsā€ but itā€™s the ā€œdumb componentsā€ that relate to the DOM and visual representation. The ā€œsmart componentsā€ exist to provide some local coordination and establish event sources so that events can enter the client logic. The only commonality between these components is that they exist in the applicationā€™s component hierarchy which is imposed by Reactā€™s rendering model.

Finally with Redux (or Context) a ā€œbackplaneā€ is introduced into the client. This makes it possible for events to enter the client logic unencumbered by the constraints of Reactā€™s component hierarchy and to affect the clientā€™s view state directly and consequently update its representation.

In React:

  • Components exist to be a member of the component hierarchy
  • Ideally visual components should be ā€œdumb componentsā€
  • Ideally ā€œsmart componentsā€ should only route data to their ā€œdumb componentsā€ and provide entry points for events into the client logic.
  • Following this approach a lot of the clientā€™s ā€œintelligenceā€ will end up in your ā€œbackplaneā€, not the components.

Now Vueā€™s Single File Components are popular primarily because it captures the three facets of the visual entity in one place: markup, styling, and behaviour. However that glosses over that the file simply becomes pure JavaScript with all the potential runtime drawbacks of that approach. The other issue is that some CSS may end up in the wrong place:

i.e. there is the temptation the include skin or cosmetic styling inside the component when that is something that should be controlled from the page itself.

So what Iā€™m driving at is that it may be necessary to drop back down to itemizing

  • data to representation transforms for each part of the page
  • events on the page that may effect change on any part of that page (actions)

For anything sufficiently large some boundaries will have to be established in order to manage complexity but the mental model of ā€œvisual componentsā€ may not be optimal in the long run.

By moving from Ruby to Elixir you have already left behind the notion that classes and objects are an essential ingredient for dealing with complex logic.

Maybe when it comes to web pages ā€œvisual componentsā€ arenā€™t all they are cracked up to be (Web Components are tied to the DOM - I think custom element describes the intent better - ā€œapp in a Web Componentā€ is likely a misuse of the concept).

Just a thought ā€¦

6 Likes

Sure, but Iā€™m just asking whatā€™s the approach here. I would be perfectly happy with answer that ā€œwe donā€™t need componentsā€ and everything is doable in a sane way without use of components.

Dave Thomas has been talking about component-oriented development and Elixir.

Would this be a good fit for LiveView?

2 Likes

:purple_heart: ah, of course, most awesomeā€¦

For those concerned its:

add {Registry, keys: :duplicate, name: :my_registry} to the children in Demo.Application

add {:ok, _} = Registry.register(:my_registry, "#{:erlang.pid_to_list(socket.parent_pid)}_image_child", []) to the mount (inside if connected?(socket)) in DemoWeb.ImageLive

add to DemoWeb.ImageLive

  def handle_info({:update, val}, socket) do
    {:noreply, assign(socket, width: val*100, bg: "blue")}
  end

and finally in the handle_event(incā€¦) in the Counter

    Registry.dispatch(:my_registry, "#{:erlang.pid_to_list(self())}_image_child", fn entries ->
     for {pid, _} <- entries, do: send(pid, {:update, socket.assigns.val})
    end)

much refactoring applies of courseā€¦

#{:erlang.pid_to_list(socket.parent_pid)} does look like a bit of a code smell, but that has nothing to do with LiveViewā€¦

3 Likes

@hubertlepicki you have very valid points.

My first impression is very positive and promising for a lot of use cases.
Keep in mind, that as soon as your customer loses the connection, the interaction with your page is also lost. So like already often pointed out from @chrismccord, it is not a perfect fit for everything.
I see it more as an enhancement or additional option to existing things like vanilla.js or react, rather then a replacement.
But used in the correct places and situations I feel lilke you get some super powers :superhero:.

I am very certain that several things you are mentioning can be done with LiveView in the same manner as in React. For many of my projects, liveView is an excellent tool of choice.
But like with every new framework i takes some time and code, to develop examples and find best practices. It would be nice if we could build frameworks with everything in mind, but only time will show how it grows and evolves (Also react is therefore a very good example.).
I am also very curious how components will be structured and build, and how all the pieces will fit together in large and continuously growing applications.

This is possible and i already did it with a TodoMVC example.
My TodoList maps over todos and uses render/2 to render from a LiveView template.leex file in the same manner i would implement it in react.
The initial state is created in the mount/2 callback and then passed via assign(socket, :todos, todos0.

GenServers, Elixirs capability of extracting Applications and using the PubSub System for subscription are a perfect fit in my opinion for designing UI scenarios and could match to some degree with state management like redux or mobx.

I suppose it is also totally valid to mix normal .eex and .leex templates and use the appropriate tool for the job.

@peerreynders thx for the detailed thoughts and explanations.
and of course thx @chrismccord for giving us this awesome tool to play with :partying_face:

1 Like

Have you got some code you can share yet? :slight_smile:

Sry not really but i am still working on it, i keep you updated and show you what i did soon :slight_smile:

But i got all my inspiration from these repos and examples

The todo-example with liveView but not really react like component usage

The newly added user-example is also great.
PubSub usage for a state management and usage of templates and updates/creations. Also there is a parent child component dependency here for example.
Also in that example there is a mix of single file liveViews ala react and the template extraction

And i also love this example, since it uses a normal controller and not the live view sugar.
For example i create the todo-genServer in the controller, pass the pid in the session and use it on mount/2 in a TodosLive.Index module.
In the view i create some navigation links for example these ones:
PhxClientWeb.Router.Helpers.todos_path(PhxClientWeb.Endpoint, :index),
PhxClientWeb.Router.Helpers.todos_path(PhxClientWeb.Endpoint, :active),
PhxClientWeb.Router.Helpers.todos_path(PhxClientWeb.Endpoint, :completed)

Overall i feel there is already a lot of options and possibilities provided in these tutorials. Still how it all comes together in a big project, thatā€™s the challenging part.

But i wanna be honest it is not very smooth for me to create these things for me and i can totally understand your point of view.
Very happy that you asked those questions.

Update:
Another very react like separation i found here for the radio_tag

4 Likes