Writing Idiomatic Code (starring Vue)
Published:
In my current job working for a contracting company, I am currently working on a client project written in Vue. Not the current Vue3, this is all Vue2 - which is end of life in just another week or two. The client needed a number of fixes for accessibility concerns to meet legal requirements, so the team I am working with was brought in to help out. Unfortunately, it's ended up as a sort of big ball of mud, rather than a well structured project; it has reams of contributions from countless developers over its 4-5 years of life, dependencies that haven't been updated since they were installed, and a test suite that only works reliably in the pipeline (although fortunately individual test suites are generally usable). Still, we're doing what we can to address the immediate problems. Hard not to daydream of what we could do if we had the time, money and autonomy to modernize it, but it's destined for retirement and rewriting in frameworks more broadly supported throughout the client company, such as React, so our work may be the last significant update this project receives.
What Makes Readable Code?
Getting into this project has been the first time I've dealt with Vue, although in the past I've worked with both React and Angular/AngularJS, so at least I understand both the idea of a framework that mixes HTML and Javascript, and a few different ways that can be done. It also uses the Vuetify component library - the 1.5 version, which incidentally is the cause of a lot of the accessibility problems. (This is a good argument for keeping your dependencies up to date whenever possible! Benefit from others' work on bugs and issues, rather than finding ways to work around them...)
In the course of the past few weeks of wrapping my head around using an unfamiliar library in a large, unfamiliar code base I've noticed a few things about different approaches that developers have taken, and what works better than others - or at least what's easier to read. Vue approaches composition a little differently than I'm used to from React, and the parts of the code that are easiest to read...actually lean into this, hard. Places where idiomatic Vue is interspersed with Vue that's written more like React or other libraries take a lot more work to read, just because your mind has to shift gears from one line to the next.
Idiomatic Vue2
For example, slots. I won't pretend to be an expert on idiomatic Vue, having only gotten into using it at all in the past month, but template slots are a major feature of Vue and how it composes components. You could have a template written something like this:
<template>
<button
v-bind="$attrs"
v-on="$listeners"
class="super--classy other-great-styles"
>
<slot></slot>
</button>
</template>
<script>
export default {
name: 'ClassyButton',
}
</script>
Which you could use like this another component:
<div>
<classy-button>Click Me!</classy-button>
</div>
If all the code you are reading is written this way, it's fairly easy to read and makes good sense; you can expect basically the same thing out of classy-button
wherever you see it without much thought. However, if the next button you see is written more like this:
<template>
<button v-bind="$attrs" v-on="$listeners" class="this-has-a-style">
{{ buttonText }}
</button>
</template>
<script>
export default {
name: 'TheOtherButton',
props: {
buttonText: String,
},
}
</script>
and gets used like this:
<div>
<TheOtherButton buttonText="'Click Me!'"></TheOtherButton>
</div>
Well...In this tiny example it's still pretty easy to see what's going on, but it's a little more difficult to read, especially when everything around it is more like the first example, and this gets even more complicated when you're working with bigger, more complex templates. It's also not quite as clear at a glance what to expect buttonText
to do, although the name makes it pretty obvious; however, the first example just reads like HTML, which makes it easy. Now imagine something seeing like this in the middle of a template:
<SomeBigOlDialog
:acceptanceText="internationalized.acceptanceText"
:cancelText="internationalized.cancelText"
:title="computedDialogTitle"
:message="someComputedProperty.messageText"
@cancel="doTheCancelThing"
ref="componentNamedSteve"
>
<template v-slot:some-slot v-if="onlySteves">
<div class="definitely--classy">
{{
conditionTwelve
? getTheInternationalized('key.for.big.body.ofText')
: getTheInternationalized('key.for.small.body.ofText')
}}
</div>
</template>
<template v-slot:some-slot v-else>
<div v-for="fred in fredArray">
{{ getTheInternationalized('key.for.fredText', fred) }}
</div>
</template>
<template v-slot:activator="{ on }">
<classy-button on="on">
Click Here
</classy-button>
</template>
</SomeBigOlDialog>
This is a mix of different coding idioms, and it gets quite tricky to see what might end up where, or under which conditions you need to supply certain attributes or slots, especially as the templates going into the slots get more and more complex; you may end up cross-referencing both SomeBigOlDialog
and the file it's used in repeatedly to get a thorough understanding. Ideally, under any idiom, if the templates going into the slots get complex, they'd be separated out into separate components as well (maintaining as few responsibilities as possible, and a minimum of nesting, in each), but in the real world that doesn't always happen, and doesn't even always make sense to do, especially for one-off items. This could be refactored to be a little smoother to read by using slots for things like the title, message, and acceptance and cancel texts if possible - and would also allow Vue to use default values for those slots if not values are provided, without wasting time writing code to check for inputs and apply defaults yourself.
Final Thoughts
The exact details of what is idiomatic for the programming language or framework you're using will vary, but in general it will help future readability and maintainability if you can keep new code in a consistent idiom with old code. It may also be worthwhile, when working with a new code base, to do some experimental refactoring of places that stick out and see if you can adapt them to be more idiomatic - it will help you learn the code and make it better for future users. But also watch out for cases where there's a reason for breaking idiom, and not just a style choice: in a couple of places where I encountered something like the above, the :message
attribute was actually parsed from a translation service, and included HTML; the raw string from the translation server was something like "A bunch of words describing the action.</br></br>Are you sure you want to take the action?"
, and dropping it into the slot with {{ $t('key.for.description') }}
resulted in the </br>
tags appearing in the text rather than getting parsed as HTML. Applying it as an attribute allowed the template to use it with v-html="inputMessage"
and solve the issue. (Side note: don't directly parse HTML unless you know where it's coming from and are sure it's safe - definitely never on public user input.)
If you're further curious about what idiomatic code even is, SDKs.io has a decent article about it, along with a number of language-specific links for idioms you may find followed in some common programming languages.