myitcv.io/react is a set of GopherJS bindings for Facebook's React, a Javascript library for building interactive user interfaces.

This post details the main features in the latest "release": 2017-05-02 - CSS, stateGen and JSX goodies

JSX-like support

JSX is an embeddable XML-like syntax, that effectively allows you to write HTML (for example) in amongst your regular application code. It came to prominence when support was introduced within the React framework. Here is a simple example, taken from the React homepage:

class HelloMessage extends React.Component {
  render() {
    return <div>Hello {this.props.name}</div>;
  }
}

Its popularity widened, and perhaps most notably it is supported as a first-class citizen within the TypeScript language: embedding, type checking, and compiling JSX directly into JavaScript.

With myitcv.io/react we are not (currently) in the mood for any Go language changes, so instead we "fake" things via compile time string constants. This ultimately needs a change within the compiler for proper compile-time support (tracked in #64), so for now we provide a runtime stop-gap solution.

Here's a basic example:

func (a *AppDef) Render() r.Element {
    return r.Div(nil,
        jsx.HTML(`
        <h1>Hello World</h1>

        <p>This is my first GopherJS React App.</p>
        `)...,
    )
}

At runtime this component is rendered as if we'd written:

func (a *AppDef) Render() r.Element {
    return r.Div(nil,
        r.H1(nil,
            r.S("Hello World"),
        ),
        r.P(nil,
            r.S("This is my first GopherJS React App."),
        ),
    )
}

Or you could equivalently use jsx.Markdown:

func (a *AppDef) Render() r.Element {
    return r.Div(nil,
        jsx.Markdown(`
# Hello World

This is my first GopherJS React App.
    `)...,
    )
}

The arguments to the jsx.* functions must be compile-time string constants. To enforce this we also provide reactVet. Whilst the stop-gap solution remains this also helps to prevent security problems (non-constant values would open the door to user-provided HTML or Markdown strings).

Clearly this "compile-time string constants"-approach is limited when compared to TypeScript's native support. But it feels like an appropriate first step for now... Further tooling/compiler support might then follow.

See the godoc for more details.

Global state trees via stateGen

If you've ever used ClojureScript's Reagent you may have come across atom. Atoms provide a way to manage shared, synchronous, independent state. Reagent components can share state using atom's.

With myitcv.io/react we achieve a similar result via stateGen. stateGen translates a succinct Go-based template into a typed state tree. Here is an example template (taken from the global state example):

package state

import "myitcv.io/react/examples/sites/globalstate/model"

//go:generate stateGen

var State = NewRoot()

var root _Node_App

type _Node_App struct {
    CurrentPerson *model.Person
    Root          *_Node_Data
}

type _Node_Data struct {
    People *model.People
}

The resulting state tree is best viewed via the godoc's. It allows components to synchronously mutate and share state with other components.

I tend to enforce that the leaves of a state tree only contain immutable values/data structures. This makes reasoning about state transitions much easier and ensures that components cannot modify data "underneath" another that might share a reference to the same value/data structure.

The PersonChooser component that is part of the global state example shows how the state tree is used. A component can either reference the global variable that represents the singleton instance of the state tree, or it can reference a node/leaf from the tree. If the referencing of a node/leaf is achieved via interfaces, then the component can be made reusable (i.e. instances of that component can be passed different nodes/leaves from the state tree via props).

The PersonChooser component is not reusable despite it's props being interface-based:

type PersonChooserProps struct {
    PersonState
}

type PersonState interface {
    Get() *model.Person
    Set(p *model.Person)
    Subscribe(cb func()) *state.Sub
}

Why? The Render method directly reference the singleton state instance:

func (p *PersonChooserDef) Render() r.Element {

    ppl := sortPeopleKeysByName(state.State.Root().People().Get())

        //...

Of course this could easily be fixed by passing in a *model.People value via the props.

This is very much a first-cut of stateGen - feedback/questions/questions greatly appreciated via Github issues.

Events are now interface-based

This change is most easily understood by looking at the props type for, say, a <button>:

type ButtonProps struct {
    // ...

    OnChange
    OnClick

    // ...
}

OnClick (and OnChange) are both interface types:

type OnClick interface {
    Event

    OnClick(e *SyntheticMouseEvent)
}

This then gets used in the following way (taken from the immutable TODO app example):

func (t *MyComp) Render() r.Element {
    return r.Button(&r.ButtonProps{
        Type:      "submit",
        ClassName: "btn btn-default",
        OnClick:   click{t},
    })
}


type click struct{ t *MyComp }

func (c click) OnClick(se *r.SyntheticMouseEvent) {
    ns := c.t.State()

    ns.items = ns.items.Append(new(item).setName(ns.currItem))
    ns.currItem = ""

    c.t.SetState(ns)

    se.PreventDefault()
}

Why move away from func types on props and state? Slice, map, and function values are not comparable (per the spec). Using interface values comes at a marginal (if any) cost to the author/reader. But critically, having comparable props and state, there is a huge benefit in terms of reasoning about component updates/re-rendering behaviour.

Feedback/questions/concerns

Feedback, questions, concerns very much appreciated via Github issues or the Gophers #gopherjs Slack channel.