Does a Halogen app architecture need multiple full-fledged components to have render performance?

NB: I come from a JS/React background and am a newbie to proper FP languages. My post probably will not make sense if you don’t know React.

I read the advice that multiple Halogen components would be more performant than one monolithic component (with some helper functions for generating pieces of app’s HTML tree).

I feel my old (closed source)React app design falls in between the two options - single global state, but with rendering (IMO) as optimal as needed, I am wondering if that model is possible in Halogen, and if it is bad design in some way. I obviously am not aware of why it would be bad. I want to use it again. More below.

My React app is made of a single global state tree - an ImmutableJS tree managed by Redux - and dumb components which are functions that receive all the state and all the actions ( to fire in response to HTML events) they need, in a props argument and return some JSX (HTML) These dumb components are wrapped by higher order functions from libraries such as react-redux, reselect and recompose which help with: selecting relevant piece of the global state , and diffing state against previous version so that the wrapped dumb component is called only if relevant piece of state has changed.
The dumb components can call other dumb components. The app is a single component tree with routing. Note that arguments to leaf components don’t need to be threaded down the component hierarchy, since the react-redux higher order function that wraps each component “cheats” by pulling what is needed from global state. So the whole component tree remains dumb and doesn’t have boilerplate to thread arguments.

Single Global state seems to be an Elm idea that Redux copied, and I have liked it ever since I heard of it. In my app the total number of fields is ~100K and ~500 changes/sec to the State are pushed over a websocket from a server. The updates actually come in at 2K/sec but I have needed to apply some throttling to some of the updates to achieve performance. BTW RxJS (FRP library) makes it a one line feature.

I think this single global state is what makes possible the neat debugging addon that Elm and Redux have. It lets you observe every state change from outside, initiate changes, roll back/replay the state changes, save/restore state. It is a beautiful thing to see your application UI change back and forth as you tweak the state.

The whole UI is a stateless dumb function of one data structure. This is the kind of elegant property that I expect functional language apps to have. Am I wrong to think this is preferable to having app state sprinkled all over the app?

2 Likes

Your title asks about the performance of Halogen which I am not qualified to comment on (we use React with React.Basic.Hooks). Your writeup though talks a lot about design and best-practice, which is all I can speak to, and hopefully just to offer a different perspective as food for thought. Back when our front-end was written in TypeScript, we used a single global state (per page) and wired it together with Redux. When we moved to PureScript, we switched to React’s builtin reducers and now have state encapsulated inside individual components. It’s definitely a trade-off with advantages on both sides.

I think in a strongly typed environment like PureScript (and even TypeScript to some extent), the single global state model suffers from some additional problems that you don’t see on the dynamic side from pure JavaScript/Redux. In JavaScript, you never actually define the whole state tree in one spot, but it gets built up by Redux dynamically as components start showing up (at least as of last time I was reading up on Redux best-practices). But in a strongly typed language like Elm or PureScript, you need to have your whole state tree defined in one spot, and that introduces some extra reusability issues. Now every stateful component that gets created requires that you edit the global state. Expect git conflicts for completely unrelated changes. Expect to have to do some rewriting when you go from one instance of a component to two instances of that component because now the state for that component type needs to be indexed somehow.

Another viewpoint is think of components like functions. In a regular logic application, you want functions to take in all of the parameters they depend on instead of relying on global state, even if that parameter list is annoyingly long, or involves “threading” data up and down the call stack, because it helps reason about a function if you understand its inputs & outputs. You could write an app that uses one big state monad in every function, but your ability to reason about that app goes down because who knows what functions are having unseen impacts on unrelated functions elsewhere. In practice, it works much better to just have a few global read-only app settings (perhaps via a Reader monad) and make any stateful interactions between functions as explicit and obvious as possible. You can apply the same argument to a UI; yeah it’s a bit inconvenient to write out large props objects just like it’s inconvenient when a function has a lot of parameters, but in the long-run it helps your reasoning if you know all of the inputs that go into rendering a given section of your DOM tree. In your case, this argument might not apply since you implied that all state changes get pushed down from the server, so components aren’t really interacting much.

Both of the arguments I gave in opposition to the large global state IMO grow more severe the larger an app becomes.

But you’ve outlined in your post some of the advantages of the Elm-style architecture, and it’s certainly quite popular, so I think the important thing is to recognize the trade-offs to both approaches instead of taking a hard-line stance that one is superior to the other. (Except don’t mix-n-match within the same program IMO).

As I said at the start of my post though, I’ll have to let somebody else more qualified discuss the performance trade-offs for Halogen and how to use the Elm architecture without shooting your refresh rate in the foot.

3 Likes

Thanks @ntwilson. Some new points here for me.

The “performance” bit was secondary to my question.
It is definitely a question about design and best practice. I was curios if a single global state would work without sacrificing performance. If I understood correctly, the motivation behind the multiple components recommendation was performance.

2 Likes

Rendering only happens to component boundaries, so there is some performance advantage to using them.

You can use the lazy/memoized functions in the halogen HTML DSL for a similar effect if performance does become a problem in the single component model though - you can predicate the rendering of a subtree based on some condition or values and prevent excessive re-rendering with that.

Another option might be to make some fairly “transparent” components that use local state minimally - they’d just accept a state value as their input and have a receiver that conditionally replaces the local state when the input is actually different. So they’re not really using the local state there, they’re just caching the relevant part of the global state. And similarly just have a single query that raises messages from the component so they’re can all be handled in some parent/root component too.

5 Likes

That is what I was hoping. The “transparent” component idea sounds like a good alternative too.

My knowledge of Halogen is limited to what I gleaned from one pass through the excellent Guide. The message passing seems to happen between immediate parent and its children.
If all the true state was at the top level, It is not clear to me how the leaf components would trigger event handlers that ultimately update state. Would leaf components ‘raise’ output messages up one level of the component tree at a time?
Would it require horribly unidiomatic hacks for leaf components to directly communicate with the top level state handlers?

1 Like

Halogen isn’t meant to be dogmatic and there only being one true way to do things - it’s true some things are going to be easier because allowances are built in, but going outside of the usual component output messaging is not too unusual even in a multi-component app.

There are a couple of options for how that might be done - you could use an FRP.Event (or Effect.Aff.Bus perhaps) that your top level component subscribes to and then have the leaf/node components send messages via that, skipping out the need to bubble explicitly through each layer.

Getting the Event into the components can be done either by explicit passing via input again, or you could use Reader to make it available everywhere through component eval.

You might want to take a look at @thomashoneyman’s Real World Halogen - it uses a reader with a bus as part of its implementation, albeit in a multi-component setting.

2 Likes

Great. That is useful to know.

Absolutely. I have already read the comments in Main and a few other files.

1 Like