Refactoring JavaScript: How to Avoid Technical Debt While Working with Legacy Code

#
By Peter Tasker

Do you have a legacy JavaScript codebase you’re avoiding refactoring? So does WordPress. If you’ve been paying attention to WordPress core lately, you may have noticed that there has been an increase in discussion about JavaScript and how to refactor legacy JavaScript while adding new features. There have already been a few JavaScript meetings in the #core Slack channel. It’s early days, but WordPress is poised to get a major overhaul in the JavaScript department. The new editor project, titled Gutenberg, Customizer updates and a REST API powered wp-admin are three big focus areas planned for future releases.

The discussions around these focus areas, especially the Gutenberg project and REST API implementation brought to mind the larger issue of refactoring a large JavaScript codebase. Where do you even start adding new features and frameworks in a massive JavaScript codebase like WordPress? In this article we’ll talk about some approaches to managing this kind of refactor, go over some of the specific options when refactoring JavaScript, as well as run through some updates that can be made to your codebase right now. Ready to modernize that massive main.js file? Let’s get to it!

Technical Debt (or why some of your code looks like it’s from 1997)

If you’re like most developers, you’ve probably had to work with a legacy codebase that was written a few years(!) ago. A few years is like a few centuries in the JavaScript world, so most of the time you would try to avoid touching this old code and carefully add new features without disturbing the mess.

To understand the issues that can arise from this approach, let’s imagine the following. Your boss comes to you and says “We need to add feature-x ASAP!”. So you get started on this new feature and think to yourself “I’d really like to try out Angular 4 and Typescript, maybe I’ll use them for this new feature”. So you leave the 100-year-old jQuery soup as-is (along with any old bugs) and start building new modules with Angular 4 and Webpack. Wave of the future!

Not so fast.

You’ve now increased something called technical debt. This is something pretty common in software development – it’s basically understanding that every line of code has to be maintained and reviewed going forward and that leaving issues or bugs for later while pushing forward with new features or functionality greatly increases that technical debt. The old code and bugs become debt that will have to be paid off in the future when further changes to the codebase are required. So when your boss comes back a month from now and tells you she wants you to update an old component, what do you do? Refactor and migrate the old component to Angular 4, or monkey patch it?

Of course you can create technical debt even with sticking to your original frameworks or processes, but it can increase exponentially when you switch over to a new framework or process. It’s intimately tied to refactoring and overall code quality standards.

The only way to avoid technical debt is to not write any code!

WordPress core development currently faces many of these challenges. Decisions are being made on which framework to choose for UI interactions going forward as well as what the approach should be for legacy and new code.

The Case of WordPress

With perhaps one of the larger JavaScript codebases in use on the internet, WordPress faces a challenge in planning future features and releases. With the new release schedule, features are at the center of each major release and there is pressure to release quickly, while adding major new features.

If you venture to take stock of the current JavaScript used by WordPress core, you might be a little overwhelmed. A recent survey by Adam Silverstein lists 38,191 lines, 95 total JS files, 16 of which are using the Backbone.js library.

WordPress isn’t immune to balancing technical debt and refactoring with the implementation of new features. As there aren’t all that many JavaScript bugs logged, it could be argued that technical debt is less of a problem for WordPress, or at least has been handled well. However, debt arises from adding new frameworks and libraries while not refactoring older logic to fit the new model.

A recent example of framework fragmentation is Project Gutenberg. This is the project to update the WordPress editor to “create a new page and post building experience.” The goal is to replace shortcodes and custom HTML with “blocks” and a more flexible editor interface.

The Gutenberg team has created a prototype that relies on the React framework. React is not currently used in core, so it would mean adding an additional framework. Furthermore, React is a fundamentally different framework than Backbone or jQuery – the two frameworks currently used in WordPress core.

The decision to use React highlights the real problem of prioritizing features over consistency and the added weight of an additional framework. An interesting post on Hacker Noon highlights minimizing dependencies as a key component to staying ‘lean’ and keeping the ability to move quickly.

When you add a library to your project, you are paying rent on that entire library and your use of it. So, you need to make a good case for every library, every plugin. Even the tiny ones. They add up fast. And if you take the ultralight, disciplined approach, you will be amazed at how quickly you can move.

By adding yet another framework to WordPress core, without a significant refactor, I’d argue that the added complexity and debt is higher than the value of the new features. In lieu of a major refactor it would make sense to at least define a standard API to link disparate JavaScript components together.

I should also note that it is still early days in terms of discussing the future of JavaScript in WordPress core, but it appears some opinions and directions are starting to form.

Time will tell if Gutenberg sticks with React and gets merged into core, but there are some tried-and-true methods for refactoring legacy JavaScript projects. Let’s see what can be done with our own legacy JavaScript.

JavaScript Refactoring Options

So we’ve seen that even WordPress core suffers from technical debt and the doom of refactoring significant portions of it’s JavaScript codebase. But what can be done to leverage modern JavaScript libraries and frameworks? It starts with a solid set of coding standards and tests. Let’s look at a few options.

JavaScript Coding Standards

The first step to minimizing technical debt and start reducing the refactor cost is to have a coding standard. For JavaScript there are plenty of projects with opinionated defaults, but it really boils down to what works for your team and project. Tools such as ESLint can help to enforce these standards and can be set up on a git pre-commit hook to ensure code maintains a consistent level of formatting. Code format and consistency helps with development speed as all code adheres to the same style making it easier to read and review.

You are doing code reviews right?

The other advantage of having a solid set of coding standards is that any updates to code, including refactoring legacy code, should adhere to the coding standards. This helps to slowly modernize old code and bring it up to the quality and consistency of new code.

Unit Tests with React

Since we’ve been talking about React in this article, we’ll go over how to set up unit tests with the de-facto React test library Jest. Jest is a pretty straight forward tool to set up, and it comes bundled with Create React App, if you’re looking to get up and running with React quickly.

This is the simple test that comes pre-configured with Create React App.

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { shallow } from 'enzyme';
import Header from './Header';

it('renders without crashing', () => {
    const div = document.createElement('div');
    ReactDOM.render(<App />, div);
});

it('renders header with props', () => {
    const header = shallow(<Header title={'What\'s up doc?'} />);
    expect(header.contains('What\'s up doc?')).toEqual(true);
});

Obviously there are more complex tests that can be run, but it shows how much additional effort it takes to get setup with unit testing. Not a whole lot! The benefit of using the Jest library is that it allows you to write tests for vanilla JavaScript as well. By writing tests for both new and old code the risk of refactoring is reduced. The goal of the test coverage is to handle any regressions or breakages along the way.

Partial Refactoring

Partial refactoring, or “cleaning as you go” is an option that many projects seem to chose for refactoring (Delicious Brains Inc. included!). This involves updating old code as you add new features and code. For example, like WordPress, let’s say you’d like to switch your view layer over to React. Let’s also say you’ve got loads of old jQuery and AJAX calls like this:

$( function() {
    $( ".my-form" ).on( 'submit', function( e ) {
        var repo = $( '.repo' ).val();
        var endpoint = 'https://api.github.com/repos/' + repo + '/issues';

        $.get( endpoint, function( data ) {

            if ( !data.length ) {
                $( '.message' ).html( 'No issues for: ' + repo );
                return;
            }
            $( '.issue-wrap' ).empty();
            $( '.message' ).empty();
            $( '.repo-name' ).html( repo );
            var details = data;

            for ( var i = 0; i < details.length; i++ ) {
                var formatted_body = details[ i ].body.split( '\n' ).join( '<br>' );
                console.log( formatted_body );
                $( '.issue-wrap' ).append( ' <div class="issue-info"><div>' +
                                           '<h3><a href=' + details[ i ].html_url + ' target="_blank">' + details[ i ].title + '</a></h3>' +
                                           '<p><small>reported by - <a href=' + details[ i ].user.html_url + ' target="_blank">' + details[ i ].user.login + '</a></small></p>' +
                                           formatted_body +
                                           '</div></div>'
                )
            }
        } ).fail( function( jqXHR, textStatus, errorThrown ) {
            console.log( errorThrown, jqXHR.status, textStatus );
            $( '.message' ).html( errorThrown + " " + jqXHR.status + " " + textStatus )
        } )
        e.preventDefault();
    } );
} );

See the Pen jQuery Github Issues lister by Peter (@tasker82) on CodePen.

Look familiar? It doesn’t to me either.

If we wanted to take an incremental approach by converting our view layer to React, we could start moving some of the string markup parts into React components.

class IssueInfo extends React.Component {
    render() {
        const details = this.props.details;

        return (
            <div className="issue-info">
                <h3><a href={details.html_url} target="_blank">{details.title}</a></h3>
                <p>
                    <small>reported by - <a href={details.user.html_url} target="_blank">{details.user.login}</a>
                    </small>
                </p>
                <div>
                    {details.body.split( '\n' ).map( ( item, key ) => {
                        return <span key={key}>{item}<br /></span>;
                    } )}
                </div>
            </div>
        );
    }
}

And set our data up as props and state data.

<div className="issues-box">
    {
        Object.keys( this.state.data ).map( key =>
            <IssueInfo key={key} index={key} details={this.state.data[ key ]} /> )
    }
</div>

And if we really wanted to start moving away from jQuery we could switch our AJAX calls to use the native fetch() method. Below is a fully refactored version in React.

See the Pen React Github Issues List by Peter (@tasker82) on CodePen.

As you can see in this simple example, it’s not super-difficult to partially swap out parts of your JavaScript code to a new process or framework. The downside of this approach is that for a time, you’ll have two or more JavaScript frameworks to keep up with. For some projects, this is an acceptable middle ground, and I have a feeling it’s the approach that WordPress core is going to take.

Throwaway Work

Another method that can be helpful in larger codebases that would otherwise require a refactor is the concept of ‘throwaway’ work. These are projects that basically act as prototypes for potential future features. Essentially, you make the decision to move quickly, skip things like code review and consistency and go off the deep end, getting the prototype complete by any means necessary. It allows developers to pick frameworks or platforms that would otherwise be out of the question. If the prototype is successful and meets all the requirements, decisions can be made about whether it should be used as-is, modified or trashed.

Prototyping this way shows that a concept is viable and potentially highlights the advantages or disadvantages of the chosen approach. In the past, I’ve used this approach to build out a pet project and prove to my manager that an idea was viable. This also looks to be what they’re doing with the Gutenberg project and I’d be super pumped if it did get accepted into core.

There are risks to prototyping like this however. A commitment needs to be made to “clean up” the code before making it to production. If this step is skipped you’re back to creating more technical debt.

None of the Above

Of course, the other option is to simply rewrite your entire codebase using a shiny new framework. Show of hands for those that have this as an option? …😏

In all seriousness, refactoring a large codebase, especially JavaScript, is a massive job. My goal in this article was to cover some of the options I’ve found helpful. But in reality, there are many different ways to tackle the issue of legacy JavaScript and fitting in new frameworks and features. It will be interesting to see which path WordPress core takes and I’m keenly interested to hear what other approaches people have taken.

Have you taken on a JavaScript refactor? What approach did you take? Let us know in the comments!

About the Author

Peter Tasker

Peter is a PHP and JavaScript developer from Ottawa, Ontario, Canada. In a previous life he worked for marketing and public relations agencies. Love's WordPress, dislikes FTP.