Concepts
The vocabulary and mental model behind HLive's server-side virtual DOM.
Tag
A static HTML tag. A Tag has a name (e.g., a <p></p>'s name is p). A Tag can have zero or more Attributes. A Tag can have child Tags nested inside it. A Tag may be Void, which means it doesn't have a closing tag (e.g., <hr>). Void tags can't have child Tags.
Attribute
An Attribute has a name and a value (e.g., href="https://example.com" or disabled="").
CSS Classes
The HLive implementation of Tag has an optional special way to work with the class attribute. These types are all designed to make toggling CSS classes on and off easy.
HLive's ClassBool is a map[string]bool type. The key is a CSS class, and the value enables the class for rendering if true. This allows you to turn a class on and off (e.g. l.ClassBool{"foo": true, "bar": true, "fizz": true}). The order of the class names in a single ClassBool is NOT respected. If the order of class names is significant, you can add them as separate ClassBool elements, and the order will be respected. You can add new ClassBool elements with the same class name, and the original ClassBool element will be updated.
Even better is Class, a string type that converts into a ClassBool (e.g. l.Class("foo bar fizz")). The order of the class names is respected. Each class can still be turned off individually using a ClassBool of the ClassOff string type.
ClassList and ClassListOff are string slices that will enable or disable, respectively, CSS classes (e.g. l.ClassList{"foo", "bar", "fizz"}).
Style Attribute
The HLive implementation of Tag has an optional special way to work with the style attribute.
HLive's Style is a map[string]interface{} type. The key is the CSS style rule, and the value is the value of the rule. The value can be a string or nil. If nil, the style rule gets removed.
The order of the style rules in a single Style is NOT respected. If the order of rules is significant, you can add them as separate Style elements, and the order will be respected.
Tag Children
Tag has func GetNodes() *l.NodeGroup. This returns the children a Tag has.
This function is called many times, and not always when it's time to render. Calls to GetNodes must be deterministic. If you've not made a change to the Tag, the output is expected to be the same.
This function should not get or change data. For example, no calls to a remote API or database should happen in this function.
Components
A Component wraps a Tag. It adds the ability to bind events that primarily happen in the browser to itself.
EventBinding
An EventBinding is a combination of an EventType (e.g., click, focus, mouseenter), with a Component and an EventHandler.
EventHandler
The EventHandler is a func(ctx context.Context, e Event) type.
These handlers are where you can fetch data from remote APIs or databases.
Depending on the EventType you'll have data in the Event parameter.
Node
A Node is something that can be rendered into an HTML tag. For example, a string, Tag, or Component. An Attribute is not a Node, as it can't be rendered to a complete HTML tag.
Element
An Element is anything associated with a Tag or Component. This means that, in addition to Nodes, Attribute and EventBinding are also Elements.
Page
A Page is the root element in HLive. There will be a single page instance for a single connected user.
Page has HTML5 boilerplate pre-defined. This boilerplate also includes HLive's JavaScript.
HTML vs WebSocket
When a user requests a page, there are two requests. The first is the initial request that generates the page's HTML. The second is to establish a WebSocket connection.
HLive considers the initial HTML to be the Server Side Rendering phase (SSR). This SSR request will not be used when processing WebSocket requests. This render is a good candidate for use in a CDN.
When an HLive SSR page is loaded in a browser, the HLive JavaScript library will kick into action.
The first thing the JavaScript will do is establish a WebSocket connection to the server. This connection is made using the same URL with ?hlive=1 added to it. Due to typical load balancing strategies, the server that HLive establishes a WebSocket connection to may not be the one that generated the SSR page.
PageSession
When the JavaScript establishes the WebSocket connection, the backend will create a new session and send down the session id to the browser.
A PageSession represents a single instance of a Page. There will be a single WebSocket connection to a PageSession.
PageServer
The PageServer is what handles incoming HTTP requests. It's an http.Handler, so it can be used in your router of choice. When PageServer receives a request, if the request has the hlive=1 query parameter, it will start the WebSocket flow. It will create a new instance of your Page, then make a new PageSession. Finally, it will pass the request to Page's ServeWS function.
If not, then it will create a new Page, generate a complete SSR page render, return that, and discard that Page.
Middleware
It's possible to wrap PageServer in middleware. You can add data to the context as normal. The context will be passed to your Component's Mount function if it has one.
PageSessionStore
To manage all of its PageSessions, PageServer uses a PageSessionStore. By default, each page gets its own PageSessionStore, but it's recommended that you have a single PageSessionStore that's shared by all your Pages on a server.
PageSessionStore can control the number of active PageSessions you have at one time. This control can prevent your servers from becoming overloaded. Once the PageSession limit is reached, PageSessionStore will make incoming WebSocket requests wait for an existing connection to disconnect.
HTTP vs WebSocket Render
Mount is not called on SSR requests but is called on WebSocket requests.
Tree and Tree Copy
Tree describes a Node and all its child Nodes.
Tree copy is a critical process that takes your Page's Tree and makes a simplified clone of it. Once done, the only elements in the cloned Tree are Tags and Attributes.
WebSocket Render and Tree Diffing
When it's time to do a WebSocket render, no HTML is rendered. (1) What happens is a new Tree Copy is created from the Page. This Tree is compared to the Tree that should be in the browser. The differences are calculated, and instructions are sent to the browser on updating its DOM with the new Tree.
(1) except Attributes, but that's just a convenient data format.
First WebSocket Render
When a WebSocket connection is successfully established, we need to do 2 Page renders. The first is to duplicate what should be in the browser. This render will create a Tree Copy as if it were going to be an SSR render. This Tree is then set as the "current" Tree. Then a WebSocket Tree Copy is made. This copy will contain several attributes not present in the HTML Tree. Also, each Component in the Tree that implements Mounter will be called with the context, meaning the Tree may also have more detail based on any data fetched. This render will then be diffed against the "current" Tree, and the diff instructions sent to the browser like normal.
For an initial, successful Page load, there will be 3 renders: 2 HTML renders and a WebSocket render.
AutoRender and Manual Render
By default, HLive's Component will do a render every time an EventBinding is triggered.
This behaviour can be turned off on a Component by setting AutoRender to false.
If you set AutoRender to false you can manually trigger a WebSocket render by calling hlive.Render(ctx context.Context) with the context passed to your handler.
Local Render
If you want to render only a single Component and not the whole page, you can call hlive.RenderComponent(ctx context.Context, comp Componenter). You will also want to set any relevant Components to AutoRender false.
Differ
Differ is HLive's tree-diffing engine. Its entry point, Trees(selector, path string, oldNode, newNode any) ([]Diff, error), walks an "old" tree (what the browser currently has) and a "new" tree (the latest render) in lockstep and returns a flat list of Diff values describing how to turn one into the other.
Each Diff has a Root ("doc" for the whole page, or a component's element ID for a scoped diff), a Path of child indexes locating the node under that root, a Type (DiffCreate, DiffUpdate, or DiffDelete), and a payload describing what changed: a Tag, Text, Attribute, or HTML value.
Where both sides have the same kind of node, the comparison goes type-specific: strings and HTML values are compared directly, tags are diffed attribute-by-attribute and then recursed into child-by-child, and a tag carrying an ID attribute resets the diff's root to that component so its descendants are addressed relative to the component, not the page. This is what lets a component's subtree be patched independently of the rest of the page.
The resulting []Diff is serialized to a wire message and sent down the WebSocket, where HLive's client-side JavaScript applies each instruction directly to the real DOM.
Render
Renderer is the piece that turns a tree of Nodes into HTML bytes. Its HTML(w io.Writer, el any) error method writes the opening tag, renders each Attribute (HTML-escaping values by default), recurses into the tag's children unless it's Void, and writes the closing tag. It has no diffing logic of its own — it only knows how to serialize whatever Node tree it's handed.
The walk that builds that tree lives one layer up, in a Pipeline of PipelineProcessor stages. Page keeps two pipelines: an SSR pipeline that ends by calling Renderer to produce the HTML response for a plain HTTP request, and a diff pipeline that produces an in-memory tree (no HTML writing at all) for the Differ to compare against what the browser already has.
The package-level Render(ctx) and RenderComponent(ctx, comp) functions are thin wrappers: they pull a render function out of the context and call it. Page installs those functions when a WebSocket connection is established, so calling either from inside an event handler runs the diff pipeline, diffs the result, and pushes the resulting Diffs down the WebSocket — for the whole page, or scoped to a single component.
HTML Type
HLive's HTML type is a special string type that will render what you've set, unescaped. One rule is that the HTML in HTML must have a single root node.
JavaScript
The goal of HLive is to not require the developer to write any JavaScript. As such, we have unique solutions for things like giving fields focus.
Nothing is preventing you from adding your own JavaScript. If JavaScript changes the DOM in the browser, you could cause HLive's diffing to stop working. This is also true in libraries like ReactJS.
Virtual DOM, Browser DOM
HLive is blind to the actual state of the browser's DOM. It assumes that it's what it has set it to.
Lifecycle
A Component can opt into two small interfaces: Mounter, with Mount(ctx context.Context), and Unmounter, with Unmount(ctx context.Context). A ComponentMountable (created with l.CM) gives you both, wired up through SetMount and SetUnmount.
These hooks are only triggered by the diff pipeline, never by the plain SSR pipeline. The first time a component is seen in that pipeline it's assigned its permanent element ID, and Mount fires at that same moment — once per component, no matter how many renders follow.
This is why a fresh page load does three renders in total, as described under First WebSocket Render: the initial HTTP response is one SSR render with no Mount calls. When the WebSocket then connects, Page does a second SSR render (discarding the HTML) just to record a baseline of what the browser already has, then a diff-pipeline render — the third render — where every component's Mount actually fires for the first time.
Unmount is called later, when the Page closes (for example when the user disconnects), for every component that implements Unmounter. Page also exposes separate, page-level HookMountAdd/HookUnmountAdd/HookBeforeMountAdd hooks that run around the WebSocket connect/disconnect itself — useful for page-wide setup and teardown that isn't tied to any single component's mount state.