You might have seen the news over the weekend (no, not that news): Facebook has decided not to relicense their popular JavaScript framework, React, in light of the decision by the Apache Software Foundation (ASF) to list React’s BSD+Patents license as Category X.

The ASF’s decision has forced a number of open source communities in their ecosystem to either begin the arduous process of rewriting significant portions of their codebases, or choose to leave the ASF altogether.

This post isn’t about taking sides in the dispute; it’s about protecting codebases from churn. Because let’s face it: even without situations like this, the JavaScript community has faced a tremendous amount of churn over the past decade.

JavaScript is the most widely used programming language in the world. It’s inspired millions of developers the world over to create libraries and frameworks for others to use—but that level of innovation and creativity has come at a cost. The community has churned through JavaScript frameworks at lightning speed. Take a trip down memory lane for a moment to remind yourself of some of the frameworks you are likely to have touched over the last decade, and ask yourself how many of those still exist: YUI, Ext JS, jQtouch, Raphael, Sencha, Prototype, Scriptaculous, jQuery, jQuery Mobile, Knockout, Backbone, Marionette, Angular 1, React, Angular 2, Vue.js.

That’s a lot of churn, but it’s also a lot of innovation. We don’t want to squash innovation; we want to benefit from it! Here are two simple tips that you can follow to minimize the impact that switching frameworks will have on your JavaScript codebases.

Tip #1: Separate Concerns

Although switching frontend frameworks will always require some amount of rewriting, it shouldn’t require a complete rewrite! Yet far too often, a complete rewrite seems to be the case. One of the surest ways to ensure that you’ll be forced to completely rewrite your code to switch frameworks is if you follow the Big Ball of Mud pattern—that is, if you mix all of your concerns together.

For example, it’s not uncommon to open up a React component (or Angular component, or Vue component) and see not just HTML (or JSX) and CSS, but presentation logic, business logic, and persistence and/or networking logic all jammed into it.

Not only does this make the code hard to read and hard to change, it essentially forces a complete rewrite when you want to switch to the latest hot frontend framework.

So take the time to separate concerns. Not only will it help you if/when you decide to switch frameworks, but even if you never changed the frontend framework, you’ll still be making a codebase that’s easier to maintain. For example, presentation logic, business logic, and persistence logic will all change at different rates and for different reasons. So put them in different places instead of coupling them all together.

Not sure what that might look like? For example, imagine you’re writing a simple application that lets people play rock, paper, scissors:

Sure, you could jam all the code to make this happen into a single React component—but you’re setting yourself up for failure if you do so. Take time to tease out the separate concerns.

For your business logic, you might try something like this:

function play(p1, p2, ui, roundRepo){ if (invalid(p1) || invalid(p2)){ save("invalid", p1, p2, roundRepo) ui.invalid() } else if (tie()){ ... } else if ... ... }

For the frontend, you might end up with a React component like this:

class RockPaperScissorsForm extends React.Component { constructor(){ super() this.state = {} this.submitRound = this.submitRound.bind(this) this.inputChanged = this.inputChanged.bind(this) } submitRound(e) { e.preventDefault() this.props.play(this.state.p1, this.state.p2, this, this.props.roundRepo) } inputChanged(e){ this.setState({[e.target.name]: e.target.value}) } invalid(){ this.setState({errors: locale.t("invalid"), winner: null}) } //etc.. render() { return <div> <h1>{this.state.errors}</h1> {this.state.winner} <form onSubmit={this.submitRound}> <input type="text" name="p1" onChange={this.inputChanged}/> <input type="text" name="p2" onChange={this.inputChanged}/> <input id="playButton" type="submit" value="Play"/> </form> </div> } }

We’ve used a design principle (dependency inversion) and a couple of simple patterns (observer, repository) to separate concerns. We’ve pulled the business logic (i.e., determining who won) out into a function called play that exists independent of the React component. The business logic tells the ui what happened by calling methods on it (the observer pattern, and an example of the “Tell, Don’t Ask” principle). The persistence logic has been hidden behind the “roundRepo” interface (i.e., the repository pattern).

Tip #2: Test Behavior, Not Implementation Details

Not only do many engineers end up forced to rewrite their production code completely in order to switch frontend frameworks—many of them also end up forced to rewrite their test code too!

The reason for this is that many engineers have fallen into the trap of testing implementation details instead of behavior. So they end up with test suites that know too much about the frontend framework they were using and how they were using it, and too little about the behavior of the program that they were trying to build.

The internet is filled with testing tutorials that encourage this kind of behavior. For example, if you were to follow the advice of one popular post on the Internet around testing React components with Enzyme and Mocha, you might end up with a test of the React component above that looks like this:

describe('<RockPaperScissorsForm/>', function () { it('should have a form to accept the input', function () { const wrapper = shallow(<RockPaperScissorsForm/>); expect(wrapper.find('form')).to.have.length(1); }); it('should have props for roundRepo and play', function () { const wrapper = shallow(<RockPaperScissorsForm/>); expect(wrapper.props().roundRepo).to.be.defined; expect(wrapper.props().play).to.be.defined; }); });

It’s hard for me to see the value in a test like this, and easy for me to see the problems with it. This test is tightly coupled to React. It uses enzyme to shallow render React components—and duplicates that process between the two scenarios. It looks in the rendered output to see if there’s a form—not the behavior the form makes possible. It ensures that React component has two props—but never executes any code paths that would prove they’re being utilized.

When you have test suites like this, you’ll have no choice but to rewrite your entire test suite when you switch frontend frameworks.

Instead, let’s see what it might have looked like had we focused on testing behavior:

describe("when the play logic determines it's a tie", function () { beforeEach(function () { mountApp({play: function(p1Throw, p2Throw, ui){ ui.tie() }}) }) it("then the frontend displays 'TIE'", function () { expect(page()).not.toContain("TIE") submitPlayForm() expect(page()).toContain("TIE") }) }) //etc...

Compare and contrast this test with the previous test. This test is testing the display logic; specifically, what the frontend should do when the business logic reports that the round is a tie. Notice that it uses a stub (one of the five types of test doubles) to double the business logic, instead of coupling the frontend presentation logic tests to the business logic.

This test also doesn’t have any React knowledge in it. You can’t tell from this test that we’re using React to implement the behavior. We could just as well be using Vue.js, or Angular, or even Elm! (Don’t believe me? Check out this repo. It uses the same test suite to test drive five different frontend implementations of a simple rock paper scissors application: React, Reflux, React/Redux, Vue.js, Elm).

This is a test suite that will not only survive switching your frontend framework—it will enable it! It will make it easier for you to do it, because it wasn’t aware of your frontend framework in the first place. And it will help you stay confident that your application still works after the switch—because it tests behavior, not implementation.

If you’re interested in learning more about this approach to testing, reach out to us —we run 3-day workshops that can help you and your team start to reap the benefits of TDD!