Vue.js versus React: the (self-)documentation angle - Hacking Thy Fearful Symmetry

Hacking Thy Fearful Symmetry

Hacker, hacker coding bright

Vue.js versus React: the (self-)documentation angle

created: October 23, 2017

So, lately I've been experimenting a tad with React and Vue.js. I've first tried React, then jumped to Vue, then went back to React, spent sometime there, and last week decided to return once more to Vue.

... Yeah, making up my mind isn't exactly my strongest suit. But anyway. In an effort to compare apples to apples, I've decided to pick a small application I've written in React, and convert it into Vue to see a direct equivalence between the two. The application, if you are curious, is a mix of a Dancer REST server leveraging the ORM magic of DBIx::Class that reads the sqlite database created by KMyMoney, and the React web application feeding from it and rendering pretty reports. Why I would create a full-stack application to generate bank reports because I found the ones produced natively to be lacking pizzazz is a fair question, but one that won't be addressed here.

So, after I did that conversion, what is the main verdict? In a nutshell: it's pretty much a choice between chocolate and strawberry ice creams -- there are differences, but both are more than likely to make you happy.

However, I found that the more I use them, the more Vue has an edge for producing slightly cleaner, sane code.

Piece of evidence #1: separation of concerns

One of the selling points of React is that "It's only JavaScript!". It's true, and to be fair it's very appealing. There is a reason why I kept using Mason for my Perl projects for a long time; why learn and use a template syntax that is purposefully gimpy when we can instead call on a well-known, Turing-complete warhorse?

As it turns out, with great powers come messy eating. When one can, one tends to inject blobs of code in the template code. It's fast, it's handy, it... doesn't make for easy reading.

Which is where Vue's Single Component Files shine. Through the years I've seen my share of frameworks and template systems, but I have to say that Vue is the one that makes the separation of concerns not only feel natural, but also easy. With its SCFs that keep the template and code close by, yet segregated, I find myself first prototyping the component with static HTML, then progressively replace the chunks that need to be dynamic. When Storybook is used, that can make for wicked tight iterative work.

Here's the React version of one of the components. Not horrendous by any standard.

import React      from 'react';
import _        from 'lodash';
import moment       from 'moment';
import Day        from './Day.jsx';
import MonthSummary   from './MonthSummary.jsx';
import { format_price } from './Entry.jsx';

const by_days = entries => _.sortBy( 
  _.values( _.groupBy( entries, 'postdate' ) ), '0.postdate' 
);

export default ({
  month, start_balance, entries, accounts, is_stock_account 
}) => <table>
  <thead>
    <tr className="opening_balance">
      <td colSpan={ is_stock_account ? 5 : 4 }>
        <h2>{month} - { moment(month).format('MMMM') } </h2>
      </td>
      <td className="money">{ format_price( start_balance ) }</td>
    </tr>
  </thead>
  { by_days(entries).map( (day,i) => 
      <Day key={i} entries={day} 
         accounts={accounts} is_stock_account={is_stock_account} 
       /> ) }

  <MonthSummary entries={entries} is_stock_account={is_stock_account} />
</table>;

But the template itself is kinda buried deep in the file.

import React      from 'react';
import _        from 'lodash';
import moment       from 'moment';
import Day        from './Day.jsx';
import MonthSummary   from './MonthSummary.jsx';
import { format_price } from './Entry.jsx';

const by_days = entries => _.sortBy( 
  _.values( _.groupBy( entries, 'postdate' ) ), '0.postdate' 
);

export default ({
  month, start_balance, entries, accounts, is_stock_account 
}) => <table>
  <thead>
    <tr className="opening_balance">
      <td colSpan={ is_stock_account ? 5 : 4 }>
        <h2>{month} - { moment(month).format('MMMM') } </h2>
      </td>
      <td className="money">{ format_price( start_balance ) }</td>
    </tr>
  </thead>
  { by_days(entries).map( (day,i) => 
      <Day key={i} entries={day} 
         accounts={accounts} is_stock_account={is_stock_account} 
       /> ) }

  <MonthSummary entries={entries} is_stock_account={is_stock_account} />
</table>;

Code in the middle of the template is meh.

import React      from 'react';
import _        from 'lodash';
import moment       from 'moment';
import Day        from './Day.jsx';
import MonthSummary   from './MonthSummary.jsx';
import { format_price } from './Entry.jsx';

const by_days = entries => _.sortBy( 
  _.values( _.groupBy( entries, 'postdate' ) ), '0.postdate' 
);

export default ({
  month, start_balance, entries, accounts, is_stock_account 
}) => <table>
  <thead>
    <tr className="opening_balance">
      <td colSpan={ is_stock_account ? 5 : 4 }>
        <h2>{month} - { moment(month).format('MMMM') } </h2>
      </td>
      <td className="money">{ format_price( start_balance ) }</td>
    </tr>
  </thead>
  { by_days(entries).map( (day,i) => 
      <Day key={i} entries={day} 
         accounts={accounts} is_stock_account={is_stock_account} 
       /> ) }

  <MonthSummary entries={entries} is_stock_account={is_stock_account} />
</table>;

And those iterations really get clunky fast.

import React      from 'react';
import _        from 'lodash';
import moment       from 'moment';
import Day        from './Day.jsx';
import MonthSummary   from './MonthSummary.jsx';
import { format_price } from './Entry.jsx';

const by_days = entries => _.sortBy( 
  _.values( _.groupBy( entries, 'postdate' ) ), '0.postdate' 
);

export default ({
  month, start_balance, entries, accounts, is_stock_account 
}) => <table>
  <thead>
    <tr className="opening_balance">
      <td colSpan={ is_stock_account ? 5 : 4 }>
        <h2>{month} - { moment(month).format('MMMM') } </h2>
      </td>
      <td className="money">{ format_price( start_balance ) }</td>
    </tr>
  </thead>
  { by_days(entries).map( (day,i) => 
      <Day key={i} entries={day} 
         accounts={accounts} is_stock_account={is_stock_account} 
       /> ) }

  <MonthSummary entries={entries} is_stock_account={is_stock_account} />
</table>;

Contrast with the vue component.

<template>
  <table>
    <thead>
      <tr class="opening_balance">
        <td :colSpan="is_stock_account ? 5 : 4">
        <h2>{{month}} - {{ month | named_month }}</h2>
    </td>
    <td class="money">
      <Money :amount="start_balance" /></td>
    </tr>
    </thead>
    <Day v-for="day in days"
      :entries="day" :accounts="accounts" 
      :is_stock_account="is_stock_account" />
    <MonthSummary :entries="entries" :is_stock_account="is_stock_account" />
  </table>
</template>

<script>
import moment        from 'moment';
import Money         from './Money.vue';
import MonthSummary  from './MonthSummary.vue';
import Day           from './Day.vue';

const named_month = month => moment(month).format('MMMM');

const days = function() {
  return _.sortBy( _.values( _.groupBy( this.entries, 'postdate' ) ), '0.postdate' );
};

export default {
  props:      [ 'month', 'start_balance', 'entries', 
                'accounts', 'is_stock_account' ],
  components: { MonthSummary, Money, Day },
  filters:    { named_month, },
  computed:   { days },
};
</script>

The template is upfront and center, giving emphasis on what the component is about rather than how it's being operated behind the curtain.

<template>
  <table>
    <thead>
      <tr class="opening_balance">
        <td :colSpan="is_stock_account ? 5 : 4">
        <h2>{{month}} - {{ month | named_month }}</h2>
    </td>
    <td class="money">
      <Money :amount="start_balance" /></td>
    </tr>
    </thead>
    <Day v-for="day in days"
      :entries="day" :accounts="accounts" 
      :is_stock_account="is_stock_account" />
    <MonthSummary :entries="entries" :is_stock_account="is_stock_account" />
  </table>
</template>

<script>
import moment        from 'moment';
import Money         from './Money.vue';
import MonthSummary  from './MonthSummary.vue';
import Day           from './Day.vue';

const named_month = month => moment(month).format('MMMM');

const days = function() {
  return _.sortBy( _.values( _.groupBy( this.entries, 'postdate' ) ), '0.postdate' );
};

export default {
  props:      [ 'month', 'start_balance', 'entries', 
                'accounts', 'is_stock_account' ],
  components: { MonthSummary, Money, Day },
  filters:    { named_month, },
  computed:   { days },
};
</script>

Formatting is addressed by filters, which are more visually distinct.

<template>
  <table>
    <thead>
      <tr class="opening_balance">
        <td :colSpan="is_stock_account ? 5 : 4">
        <h2>{{month}} - {{ month | named_month }}</h2>
    </td>
    <td class="money">
      <Money :amount="start_balance" /></td>
    </tr>
    </thead>
    <Day v-for="day in days"
      :entries="day" :accounts="accounts" 
      :is_stock_account="is_stock_account" />
    <MonthSummary :entries="entries" :is_stock_account="is_stock_account" />
  </table>
</template>

<script>
import moment        from 'moment';
import Money         from './Money.vue';
import MonthSummary  from './MonthSummary.vue';
import Day           from './Day.vue';

const named_month = month => moment(month).format('MMMM');

const days = function() {
  return _.sortBy( _.values( _.groupBy( this.entries, 'postdate' ) ), '0.postdate' );
};

export default {
  props:      [ 'month', 'start_balance', 'entries', 
                'accounts', 'is_stock_account' ],
  components: { MonthSummary, Money, Day },
  filters:    { named_month, },
  computed:   { days },
};
</script>

And yeah, that's a list iteration that is softer on the eye.

<template>
  <table>
    <thead>
      <tr class="opening_balance">
        <td :colSpan="is_stock_account ? 5 : 4">
        <h2>{{month}} - {{ month | named_month }}</h2>
    </td>
    <td class="money">
      <Money :amount="start_balance" /></td>
    </tr>
    </thead>
    <Day v-for="day in days"
      :entries="day" :accounts="accounts" 
      :is_stock_account="is_stock_account" />
    <MonthSummary :entries="entries" :is_stock_account="is_stock_account" />
  </table>
</template>

<script>
import moment        from 'moment';
import Money         from './Money.vue';
import MonthSummary  from './MonthSummary.vue';
import Day           from './Day.vue';

const named_month = month => moment(month).format('MMMM');

const days = function() {
  return _.sortBy( _.values( _.groupBy( this.entries, 'postdate' ) ), '0.postdate' );
};

export default {
  props:      [ 'month', 'start_balance', 'entries', 
                'accounts', 'is_stock_account' ],
  components: { MonthSummary, Money, Day },
  filters:    { named_month, },
  computed:   { days },
};
</script>

Piece of evidence #2: Ifs and Loops

Something else that doesn't make for easy ready is how React deals with ifs and iterations over lists. Is the idiomatic way to deal with those clever? Yes, very. Do somebody at ease with JavaScript likely to have a problem understanding what is going on? Not really. Will it requires of them to pause each time and parse how things are implemented to what is implemented? I think so. It's not a huge thing, but that's what makes the difference between code that is not hard to read, and code that is easy to read.

Let's be honest. Do you prefer this?

{index === 0 && (
    <td className="day" rowSpan={size}>
        {moment(entry.postdate).format("DD")}
        <div className="weekday">{moment(entry.postdate).format("ddd")}</div>
    </td>
)}

... or that?

<td class="day" :span="size" v-if="!index">
    {{ postdate_dayofmonth }}
    <div class="weekday">{{ postdate_dayofweek }}</div>
</td>

How sharply are you tilting your head to grok this?

{ by_days(entries).map( 
    (day,i) => 
      <Day key={i} entries={day} 
         accounts={accounts} is_stock_account={is_stock_account} /> 
) }

Versus that?

<Day v-for="day in days"
    :entries="day" :accounts="accounts" 
    :is_stock_account="is_stock_account" />

Piece of evidence #3: Shadows of self-documentation

Something else that I very much appreciate about Vue is the way the components are declared as a simple object. Just by looking at it, one can have a pretty good idea what are the component's characteristics.

For example, this component accepts all those props...

export default {
  props:      [ 'month', 'start_balance', 'entries', 
                'accounts', 'is_stock_account' ],
  components: { MonthSummary, Money, Day },
  filters:    { named_month },
  computed:   { days },
};

... uses (or might use) those components within its template...

export default {
  props:      [ 'month', 'start_balance', 'entries', 
                'accounts', 'is_stock_account' ],
  components: { MonthSummary, Money, Day },
  filters:    { named_month },
  computed:   { days },
};

... uses this filter...

export default {
  props:      [ 'month', 'start_balance', 'entries', 
                'accounts', 'is_stock_account' ],
  components: { MonthSummary, Money, Day },
  filters:    { named_month },
  computed:   { days },
};

... and has one function that munges prop to get a derived value.

Got it.

export default {
  props:      [ 'month', 'start_balance', 'entries', 
                'accounts', 'is_stock_account' ],
  components: { MonthSummary, Money, Day },
  filters:    { named_month },
  computed:   { days },
};

In fact, those component objects are so descriptive...

...one could be almost tempted to write code that gathers that information for all the components an app use...

import _ from 'lodash';

let Month = require('./Month.vue').default;

function add_component ( comps, component ) {
    if ( !component ) return comps;

    comps[ component.__file ] = component;
    if ( component.hasOwnProperty('components') ) {
        _.values(component.components).map( c => add_component( comps, c ) );
    }
    return comps;
}

let components = add_component({},Month);

... and feed them to a meta-app...

<template>
  <div>
    <Component v-bind="component" 
        v-for="component in all_components" />
  </div>
</template>

<script>
import Component from './Doc/Component.vue';
import _ from 'lodash';
import add_component from './utils';

let Month = require('./Month.vue').default;

let components = add_component({},Month);

export default {
    components: { Component },
    data: () => ({ all_components: components })
};
</script>

... that grooms them into self-generated documentation.

<template>
  <div>
    <h2><a :name="__file">{{ __file }}</a></h2>

    <div v-if="props">
      <h3>props</h3>
      <ul><li v-for="prop in propNames">{{prop }}</li></ul>
    </div>

    <div v-if="components">
      <h3>components</h3>
      <ul><li v-for="(comp,name) in components">
          <a :href="'#' + comp.__file">{{ name }}</a>
      </li></ul>
    </div>
  </div>
</template>

<script>
export default {
  props: [ '__file', 'props', 'components' ],
  computed: {
    propNames: function() {
      return this.props instanceof Array ) ? this.props 
             : Object.keys(this.props);
    },
  }
}
</script>

Yes, one could almost be tempted to do that. But that'd be silly.

Also look like this:

introspected documentation

Enjoy!

comments powered by Disqus

About the author

Yanick Champoux
Perl necrohacker , ACP writer, orchid lover. Slightly bonker all around.