gomponents: declarative view components in Go

Written 25th of September 2020, updated 2nd of November 2020

I miss building views like in the React/Javascript world when in Go-land.

There's something so natural about thinking your view in terms of being built out of components. Everything from the buttons you use everywhere, to the big, interactive pages that pull everything together, it just feels easy to build pages from these building blocks.

In React, components render to HTML DOM elements based on state passed to them from parent components. This state is called properties, or props in short. Ideally, they are pure components, which means that they only change how they look and behave based on those props, just like pure functions without side-effects. They are reactive to the data passed in, which is why it's called React.

This way of building views enables a very declarative style of code, where you write what your component should do and look like based on the current props, instead of how it should change based on changes in props. For example, let's say you're building a component that shows a list (in Javascript/JSX):

const List = ({items}) => (
  <ul>
    {items.map(item => (<li key={item.id}>{item.text}</li>))}
  </ul>
)

See how there are no addItem or removeItem functions? It's just, given this list of items, render these DOM elements. It's simple and easy to reason about.

In Go, we have various templating languages, like template/html in the standard library. You can sort-of kind-of build components using the {{template}} and {{define}} syntax, but passing props isn't so easy.

Yes, there's the pipeline, but defining a new pipeline to pass to a template from within another template is hard. It works for simple cases, like passing in a string (for example, template "headline" "Hello World"), but forget about building complex props.

Introducing gomponents

I wanted to explore building view components like this, but in server-side Go. I call the project gomponents, short for Go DOM components, and it's available on Github, of course.

The core idea is pretty simple and obvious: components are just composed of DOM nodes and other components. These components should be able to render themselves to HTML. That's it.

Let's start with an example. In Go with gomponents, the List component from the Javascript example above would look like this:

import (
  g "github.com/maragudk/gomponents"
  "github.com/maragudk/gomponents/el"
)

func List(items []string) g.Node {
  var lis []g.Node
  for _, item := range items {
    lis = append(lis, el.Li(g.Text(item)))
  }
  return el.Ul(lis...)
}

Unfortunately, we don't have the handy map function like in Javascript (until we get generics 🤞), but the core principle still stands: data in, elements out, declaratively.

To enable this style, gomponents provides a Node interface that has just one function, Render(w io.Writer) error, that has the responsibility of rendering the node to an HTML string representation:

type Node interface {
  Render(w io.Writer) error
}

There are a lot of DOM element helpers (like el.Ul and el.Li above), which just know how to render themselves to HTML, with attributes, content, etc. These helpers are actually a majority of the code base, just to make it easier to build components.

(Are the helpers boring for me to write in the library? Yes. Very much.)

Anyway, the Node interface enables composing components of basic DOM nodes together with other, custom components:

func Page(title string, items []string) g.Node {
  return el.Document(
    el.HTML(
      g.Lang("en"),
      el.Head(
        el.Title(title),
      ),
      el.Body(
        el.H1(title),
        List(items),
      ),
    ),
  )
}

In the end, components can be rendered in your handlers with something like this:

func(w http.ResponseWriter, r *http.Request) {
  p := Page("Your sweet hats", []string{"Turtlehat", "Partyhat"})
  _ = p.Render(w)
}

…and that's it! To explore gomponents, have a look at the examples, or try it locally with go get -u github.com/maragudk/gomponents.

Closing thoughts

I'm still in the exploring phase of the project, figuring out whether this is a good idea at all. It's certainly not as pretty to write as when using a templating language or with JSX, but on the other hand it's pretty clear what's going on, which feels Go-idiomatic. If this turns out to be a good idea, maybe we can invent GOX as a counterpart to JSX…

And finally, I have a request for you: if you know of a library like this already, please send it my way, either on Twitter at @markusrgw, or to markus@maragu.dk. I'm sure I'm not the first to think of this way of building views in Go, but my search has turned out empty so far.

If you liked this blog post, please share a link to it, along with your thoughts and comments. If it's on Twitter, make sure to tag me as well. I'm @markusrgw on there.

About me

A picture of Markus.

I'm Markus, a professional software consultant and developer. 🤓✨ You can reach me on markus@maragu.dk or at @markusrgw. If you like my work, consider sponsoring me. You could also check out my LinkedIn profile, or do neither of these things and just grab a coffee and a biscuit.

Comments

Add comment

Newsletter

Is what I write useful to you? Sign up to my newsletter for software and business insights directly in your inbox. 😄 You could also use plain old RSS.