React.js from Scratch

In Flux from Scratch, we made a nice simple data management system using the big concept of Flux: unidirectional data flow. Then at the end of the post I made a quick example using it that just slapped together some jQuery soup to render a todo list interface and handle toggling/removing/adding todos.

This is the same approach I took back in 2009 when writing Bugrocket, and since then every time I make a change I inevitably create tricky bugs. As the sole developer on the project, this is a big problem and makes the QA/release cycle a lot longer than it really needs to be.

It also makes testing extremely difficult, server side rendering practically impossible, and it’s kinda slow!

Imperative → Declarative

In React.js we have a better way to write interfaces. When our todo app loads, we have to take actions like this:

  • create or find an appropriate ul
  • for each item in the todos array
    • create an li based on the todo’s contents
    • find the .toggle element, add an update handler
    • find the .remove element, add a remove handler
    • insert it into the ul
  • wire up an event handler for the ‘new todo’ form

Then on updates we need to:

  • find that ul
  • find the right child li given the todo we’re updating
  • modify the contents according to the new values

With React there’s only 1 step for both loading and updating:

  • re-render the entire interface from scratch

Wait… re-rendering the whole thing must be worse, right? Well, it would be, except that we aren’t just shoving HTML strings into the DOM. Instead we use a virtual representation of the DOM, remake it each time, and automatically only update what needs to change to make the real DOM match.

Basically we want to specify what the UI should look like with a pure function. With the same arguments/inputs the UI will be the same. Given that fact we can re-run the function any time, compare the output to the real DOM, and update carefully. That’s how we’ll avoid all of the jQuery soup problems.

It will be more testable, easier to reason about, potentially renderable server-side, and faster.

Writing UI in JavaScript

So what kind of virtual representation should we have? We’re going to aim for this:

{
  tag: 'div',
  attrs: { className: 'our-app' },
  children: [
    {
      tag: 'span',
      attrs: null,
      children: [
        { tag: 'span', attrs: null, children: ['Hello '] },
        { tag: 'em', attrs: null, children: ['World!'] }
      ]
    }
  ]
}

Well that’s fine, but really annoying to write and pretty ugly. Instead let’s write function calls to a helper to generate this more succinctly.

dom( 'div', { className: 'our-app' },
  dom( 'span', null,
    'Hello ',
    dom( 'em', null, 'World!' )
  )
)

Much better. Maybe this is enough syntactic sugar and looks good to you already… If not, we can do one better – by using Babel we get JSX support (not to mention some fancy ES6 features).

The dom function signature above is no accident. It’s exactly what Babel will generate for JSX like this:

<div className='our-app'>
  <span>Hello <em>World!</em></span>
</div>

Very nice! Here’s that dom function. It does some extra bits like making sure that non-orphan string nodes are turned into spans instead – used for tracking them as elements we can ninja-update later. It also supports function-as-tag so we can write <TodoItem todo={todo} />.

function dom( tag, attrs /*, children */ ) {
  if( typeof( tag ) == 'function' ) {
    // allows magic like <MyApp someProp='wow!' />
    attrs = attrs || {}
    attrs.children = children
    return tag( attrs )
  }
  else {
    var children = [].concat.apply(
      [], Array.prototype.slice.call(arguments).slice(2)
    )

    if( children.length > 1 ) {
      // if we have multiple children we need to convert
      // any text-only nodes into spans for tracking purposes
      for( var i in children ) {
        var child = children[i]
        if( child && typeof(child) != 'object' ) {
          children[i] = dom( 'span', null, child.toString() )
        }
      }
    }

    return { tag: tag, attrs: attrs, children: children }
  }
}

Virtual DOM-y

So we have our ‘virtual DOM’ structure, but it’s still missing one important piece. Since we need to track the elements and only update the bits that have changed in the actual DOM, we need some kind of unique ID for each element – and not only a unique number but more like a unique position.

To do this we’ll take the same approach as Facebook does with React and describe where the element is with depth-wise IDs like ui.X.Y.Z. Our final virtual DOM structure will be:

{
  tag: 'div',
  _depth: [0],
  attrs: { className: 'our-app' },
  children: [
    {
      tag: 'span',
      _depth: [0, 0],
      attrs: null,
      children: [
        { tag: 'span',
          _depth: [0, 0, 0],
          attrs: null,
          children: ['Hello '] },
        { tag: 'em',
          _depth: [0, 0, 1],
          attrs: null,
          children: ['World!'] }
      ]
    }
  ]
}

Here’s a function to do that:

function prepare( tree, depth ) {
  depth = depth || [0]

  if( Array.isArray( tree ) ) {
    // recursively prepare arrays of elements (children)
    for( var i in tree ) {
      tree[i] = prepare( tree[i]||' ', depth.concat( i ) )
    }
  }
  else if( typeof( tree ) == 'string' ) {
    // string elements are at the bottom of the tree
    // and can just be inserted directly with no preparing
  }
  else {
    // 'tree' is a single element object

    // calculate depth/unique path to element
    // for tracking purposes
    tree._depth = [].concat( depth )

    // prepare children
    if( tree.children ) {
      tree.children = prepare( tree.children, depth )
    }
  }

  return tree
}

Rendering

Finally, we need a way to take that structure and either add DOM nodes to the target spot in the real DOM, or update existing ones. This is the beefy one so get ready. The comments inline should hopefully explain pretty well what’s going on here.

function renderNodes( nodes, target ) {
  if( Array.isArray( nodes ) ) {
    for( var i in nodes ) {
      renderNodes( nodes[i], target )
    }
  }
  else {
    // actually just a single node
    var node = nodes

    if( node === null ) {
      // whitespace between elements
      target.appendChild( document.createTextNode(' ') )
    }
    else if( typeof( node ) == 'string' ) {
      // at the bottom of the tree we have plain
      // text nodes, they are guaranteed to be single
      // children, so just set inner html of the container
      if( target.innerHTML !== node ) { target.innerHTML = node }
    }
    else {
      var el

      // find existing node first if possible
      var dataId = 'ui.'+node._depth.join('.')
      var existing = target.querySelector(
        node.tag + '[data-ui-id="'+dataId+'"]'
      )

      // the relative depth of this node in its container
      var relativeDepth = node._depth[node._depth.length-1]
      // the node at the same location in the existing DOM
      // if there is one
      var nodeAtSameLocation = target.children[relativeDepth]

      // if there's no existing node, or the node at the correct
      // place is not the *same* node, then we need a new one
      // to replace it with
      if( !existing || !nodeAtSameLocation.isSameNode(existing) ) {
        el = document.createElement( node.tag )
        el.dataset.uiId = dataId
      }
      else {
        el = existing
      }

      // various annoying attribute handling
      // IRL there would be a lot more stuff here
      if( node.attrs ) {
        var mapping = { className: 'class' }
        var special = [ 'onChange', 'onClick', 'value' ]
        for( var i in node.attrs ) {
          if( special.indexOf(i) != -1 ) { continue }
          el.setAttribute( mapping[i]||i, node.attrs[i] )
        }
        if( node.attrs.onChange ) { el.onkeyup = node.attrs.onChange }
        if( node.attrs.onClick ) { el.onclick = node.attrs.onClick }
        if( node.attrs.onSubmit ) { el.onsubmit = node.attrs.onSubmit }
        if( node.attrs.value ) { el.value = node.attrs.value }
      }

      // recursively render the children of this node
      if( node.children ) {
        renderNodes( node.children, el )

        // after rendering children at this depth
        // we need to clean up extra old nodes
        // because otherwise they will get left around
        // untouched (we aren't 'diffing' just brute-forcing)
        if( el.children.length > node.children.length ) {
          var stop = node.children.length
          for( var i = el.children.length; i > stop; i-- ) {
            el.removeChild( el.children[i-1] )
          }
        }
      }

      // if we have no existing node, we just need
      // to append the new one
      if( !existing ) { target.appendChild( el ) }
      // otherwise drop in the new node, which might be a no-op
      else { target.replaceChild( existing, el ) }
    }
  }
}

The basic idea is that we look for an element with the same data-ui-id and, if found, compare it to the element we expect at that location in the DOM. If all is well we can update it directly. If not, we just create a new element and insert it instead.

Finally we need to put together renderNodes and prepare.

function render( tree, container ) {
  return renderNodes( prepare(tree), container )
}

This is our equivalent of React.render.

We can build an app* with this now!
* Well, a toy example app. There are a ton of edge cases, bugs and missing features in this implementation :)

TodoApp, Again

I whipped up the same todo app from last time (the relevant Flux bits are included from (here) to keep the code shorter) – but now instead of some jQuery soup generating HTML strings and clobbering the interface on every change, we now have a declarative UI that only updates what actually changed.

See the Pen React.js from Scratch by Ryan Funduk (@rfunduk) on CodePen.

Well, that’s actually a bit unexciting isn’t it? It’s exactly the same (from the pointer-and-clicker perspective) as last time! Under the hood it’s only updating the parts of the DOM that need updating, and even re-using elements where possible.

Here’s a little video of that in action which is a bit more fun.

Like in Flux from Scratch, doing this helped me gain some pretty useful insight into how React works – not to mention an appreciation for its complexity!