Part 1 of "Toward a maintainable front-end" series.
Creating browser-based client applications has become considerably more approachable and convenient. With the emergence of powerful frameworks, libraries and tooling, front-end engineers can code even the most sophisticated apps easily. But with great power comes great … mess. A large, maintainable client-side codebase is a marvel rather than the rule.
Mastery is one of the most rewarding elements of working as a software developer. Carefully crafted code can make any nerd feel warm and fuzzy, but code crafted well is what ensures its longevity and ultimately its value. So why is it so common to ship front-end code that can be considered legacy the moment it is released?
To me, the root cause of these difficulties is the incompatibility at the interface between the computer and the person using it. Front-end code functions as a proxy between the world where everything is either a 0 or a 1, and the world where nothing is so. The friction is inevitable. People feel forced to think a bit like a computer to use the app, while the code has to be a bit more fuzzy to be consumable by people. The former is the area that designers try to improve by polishing UIs and UXs to make apps as intuitive and humane as possible. The latter is the problem of the developers.
On the client side, the requirements are always fuzzy; the design and UX are changing daily. There are virtually unlimited solutions for every problem, and it’s very difficult to assess them. If you don’t take this into account at the architectural level, you end up with hacky patchwork code rather than state-of-the-art code.
That is why we have to optimize our code for the only constant — change. This sounds great, but what exactly does it mean? Let’s try to dig deeper and look at the potential sources of change resistance in client-side codebase.
The state of hell
Managing the state of a web app can be challenging. They are usually complex systems with uncountable possible combinations of internal data. In a traditional MVC app, the state is distributed between models, views, controllers, server, LocalStorage, DOM … This last case is especially destructive, as it is done very implicitly, often accidentally. It’s not obvious that anytime you write code like this:
let isActive = $el.hasClass('active');
you are using the DOM as a state container.
If there is not single source of truth, an exact reproduction of an arbitrary state of an application is often tedious. It might require changes in the database or a long procedure of clicks and keystrokes while interacting with the interface. And if we are not confident where and how things are persisted, we will not be confident changing it.
How we mutate the state also plays a large role in how maintainable our system will be. Libraries like Backbone encourage direct mutations of your persistence model, in a fashion similar to this:
state.set('title', 'Mutant'); ... state.on('change:title', handleChange);
While this works quite well for simple apps, for more complicated ones it quickly turns into a difficult game of guessing: What exactly happens when I set the new title? The answer will most likely be spread all around the codebase in a convoluted chain of events.
Another enemy of change takes a form of imperative control. Directly orchestrating the behavior of components in your app often becomes unmanageable for a large enough scale. A puppeteer can only manage so many marionettes before the strings get tangled together. A line like
in your code seems harmless, but in reality it is just another string.
Separate the right things
Separation of concerns traditionally has been present in the client-side word when taking about separating styling (as CSS stylesheets), content (as HTML or templates) and logic (in JS files). Recently, this line of thinking has been exposed as a misunderstanding of the concept of SoC, while actually being a separation of technologies. So what are the real concerns in a typical web app, and why should we try to separate them?
Instead of looking at the implementation details, we should instead identify the parts of the application that are dealing with conceptually independent tasks. Examples of such concerns are presentation, state management, validation, server syncing or tracking. All these things have to work together for the application to be usable, but there are very clear boundaries between them. We can embrace this natural separation and architecture of our app so that these concerns form building blocks, easy to change or iterate on independently.
Old wisdom, new tricks
Some qualities of code, like encapsulation, reusability, composability or testability have been long established as the foundations of maintainable software. In the relatively immature world of browser apps, these qualities are gradually rediscovered and embraced as new techniques and technologies make them more accessible and widespread.
Many new libraries and frameworks provide means of creating fully encapsulated components that span across technologies. Used considerably and paired with modern tools like Webpack, they have all you need to start creating change-friendly, decoupled building blocks for your app, with clear dependencies and interfaces. These tools are mature and battle-tested.
And here comes the final change-resistant factor: the developers. To take advantage of the latest ideas emerging in the front-end world, we need to take initiative and question the status quo. Make sure to reserve some development time to reiterate on the architecture to reinforce these universally praised qualities in your codebase. The next big thing is just around the corner, so be prepared.
About half a year ago, front-enders at issuu began the journey for maintainability. There is no finish line here, as the perfect code is always a moving target. However, some of the steps we have taken have moved us in the right direction, and we’ll be more than happy to share our experiences in upcoming posts. Stay tuned!
Part 2 of “Toward a maintainable front-end” series is here.