Trace Logging with React

Long ago, when I was a Delphi (Pascal) programmer, I encountered a neat language feature called properties, which are awesome for debugging (and more well known now, as they are in C#).

  Property Area   : Longint read fArea;
  Property Left   : Longint Index 0 read GetCoord write SetCoord;

Properties are accessed with the same syntax as a class-level variable. The consequence of this is that you can turn any variable into a property by adding three or four lines of code, which gives you a chokepoint where you can put a breakpoint to find out whenever that variable is set, which can be pretty awesome for understanding how an application works. I found this taught me to visualize an application as a plumbing problem: data goes from point A to point B, and all you need to do to fix a defect is figure out where it stops or gets mangled.

In the new world of React programming, we’re taught to treat a UI code as simply a mapping from state to DOM objects, which is similarly freeing (although without the rigidity that XSLT imposed).

If you use JSX, you can write composable components that are pretty easy to read (this is rendering a proposal editor):

return (
  
    
{sections}
);

All the state that controls the UI is passed in.

This design causes one or more large top-level objects. I split these into three classes of state: the ‘input’ data model (things from the database), the ‘output’ data model (what the end user thinks they picked), and transient UI state (which text box is focused, which tabs are open, etc).

The top level state object exposes event handlers which correspond to things a user does:

var events = {
  selectProposal: function (proposal, event, id) {
    data.ui.sectionIndex = 0;
    data.ui.selectedSections = proposal.sections;

    page.setState(data);
  }
...
}

Most of these event handlers look almost the same (using cursors may be eventually a helpful simplification).

In essence, we’ve replicated the “property” functionality that C# and Delphi have, which some fun results.

We can trivially write a function which takes the above events list and wraps each event in logging code:

function trace(events, log) {
  return _(events).reduce(
    function (accum, fn, key) {
      accum[key] = _.partial(log, fn, key);
      return accum;
    },
  {});
}

The ‘log’ function can just write to the console output, although that can get verbose fast:

log.before = function (fn, key, args) {
  console.log(key + ': ' + args.join(', '));
}

Rather than logging everything, I’ve chosen to provide pre/post event hooks – this lets you focus the log on the particular piece of state you’re interested in.

function log() {
  fn = arguments[0];
  key = arguments[1];

  var newArgs = _(arguments).slice(2);

  if (!!log.before) {
    log.before(fn, key, newArgs);
  }

  fn.apply(this, newArgs.value());
      
  if (!!log.after) {
    log.after(fn, key, newArgs);
  }
}

An interesting side effect of this approach is that you can add conditional breakpoints that fire when a value is set:

log.after = function (fn, key, args) {
  console.log('data.ui.accordionIndex: ' + data.ui.accordionIndex);

  if (data.ui.accordionIndex === undefined) {
    debugger;
  }
}

You can also record checkpoints of the top level state – this lets you return at any time to the point right before your error occurred:

log.before = function (fn, key, args) {
  console.log('data.ui.accordionIndex: ' + data.ui.accordionIndex);

  if (args[0] === undefined) {
    checkpoints.push(_.cloneDeep(data));
  }
}

Alternately, if data became wrong earlier in the process, you could record constant snapshots, allowing you to replay everything in a test case.

Overall, this style of programming is a huge improvement in working with the otherwise very chaotic nature of Javascript development.