Framework and package dependencies will kill your application.
Eventually.
A product is the result of several teams working together. A horizontal dependency happens when you integrate/adopt a solution provided by an entity outside your domain. This entity may be another enterprise team or developer(s) of an open source npm package. It doesn’t share the same values, priorities or release cadence of your product.
I have been involved in projects that developed issues within a short time span due to dependencies or shortsightedness. I’ll list some of them here.
- Your JavaScript framework. This is the primary App killer. It will not be cool after a few years. No one wants to work on jQuery, Knockout.js, Backbone, Ext.js or Ember. You are also unable to hire developers because of this. Thus a rewrite is on the horizon.
- There was no upgrade path accounted in the product dev roadmap. A new version of the framework now has breaking changes. An upgrade will involve touching every part of your code. It becomes a huge effort.
- Some of the packages are not maintained any more. In fact they now print out ominous warnings during build. Bower, anyone?
- It started as proof of concept and then grew very quickly into a big app. No one did any due diligence while adding packages left and right.
- Creating a new app is exciting. In the enthusiasm to churn out features, testing took a back seat. Months have passed and now it is a tech debt. No one is interested in writing tests retroactively. Regression risk hangs over anyone that touches the code.
- The app pioneers lost interest/left/moved-on to another project. The caliber of the new team isn’t up there yet. Parts of the code are treated like a black boxes.
- Another team was responsible for developing components or abstractions that were reused in your app. That team changed or switched priorities. Now you have to either maintain them yourselves or change your app to model after their new direction.
Open source is great but …
A while ago, running npm audit on a CRA generated app gave me this:
found 9834 vulnerabilities (1 low, 9833 high) in 909317 scanned packagesrun `npm audit fix` to fix 3584 of them.1 vulnerability requires semver-major dependency updates.6249 vulnerabilities require manual review. See the full report for details.
Not very comforting. An update is due. I had to factor that into the development roadmap. With CRA, it was easy since it is very actively maintained.
I am NOT advocating writing everything in-house. That would be insane and downright impossible. There is no need to reinvent the wheel. However, there are several things you can do to make sure that your UI application remains fairly shiny throughout its life. I’ve worked on jQuery, YUI, Backbone, Knockout, Ext.js, Angular.js and Ember in the past. My recent experience has been mostly on React. These observations and recommendations are based on that.
Keeping your application young always
Picking framework and packages
An ideal App has no external dependencies. But that’s not practical. Thus there are a few packages/libraries that you WILL need most of the time.
- A UI Framework.
- A component library. Watch out for inflated bundle sizes with these.
- A utility library — Such as lodash / ramda
- A testing framework.
- Libraries for i18n, XHR, routing, form and state management. These are problems already solved and there is no need to redo them.
Even these choices will challenge you in the future when it comes to upgrading, or enhancing your app. That is the compromise and you should do your research. Remember that just because it is published on npm means nothing.
- How many people are actively contributing to the core features? Just one? The more popular a package is, the more important this becomes. Having just a single contributor is a big risk. In the UI world, nothing is constant. Take a look at the github chart.
- Is it being actively maintained?
- Is it backed up by companies? This is a double edged sword. Big corporations can end up pushing frameworks with horrible DX. I am looking at you, Angular.
- What is the github star rating?
- What is the download trend?
- What is the bug density?
- Who owns the repo? Is there a risk of abandonment? Can it be taken over by malicious entities?
- How much will it add to your production bundle size? Certain libraries will bloat your bundle the moment you use just one function. Be careful of libraries that lets you chain functions.
- Write your own simple abstractions when possible. If your use case for consuming a REST or GraphQL endpoint is small, don’t try installing a full third party library such as Apollo over it. Keep it simple. After all they are just http requests. Don’t build an ORM if you don’t need to.
- During code reviews, pay close attention when engineers add new packages.
- If another team is providing you a dependency, find out what the support/upgrade plan for it? Short term and long term. I was part of a new React project recently but couldn’t use hooks because the underlying support libraries still used an older version of React.
Have periodic upgrade as part of the development roadmap
Left unchecked, upgrade becomes a technical debt very quickly. The more you delay this, the tougher the task becomes. Upgrading is not a trivial task, especially if your code base is large and you have horizontal teams providing support libraries. We recently had to upgrade an Ember app. After two years of sticking to the original version, it was not an easy task. It had to be coordinated between multiple teams that handled different packages.
Now a days, in enterprises, nothing is constant. Projects, people and priorities change. I’ve worked in projects where the principal architect and senior devs had left and it was now owned by a new group of junior engineers. The team didn’t understand the system well. To make matters worse, there were very little unit or integration tests written. Outside teams that depended on this product to provide components and support suffered.
Tests
Strap your code with unit and integration tests. Write functional tests for feature and workflow testing. Let QA focus on mission critical pieces such as payment forms, security, access control etc. Tie your tests to your commits and builds.
Co-locate your features. Make it easy to delete code
This means that your code organization matters. Use css-in-js or css modules. Keep the tests local. Can you delete a feature by just deleting a folder? It will never be that simple given that UI modules cross reference each other via composition or routes. Still, it is a goal worth pursuing.
Don’t put all your UI state in a single global tree
This means that a lot of planning needs to go in before you adopt a global state management library like Redux. Use context providers to keep local state small and isolated.
Don’t be too quick to abstract
Understand the problem very well before you abstract it. Wrong abstraction is worse than code duplication.
Keep your bundle size in check
Keep track of bundle size. Measure it, document it.
Linting
Linting combined with an opinionated formatter such as Prettier will help write idiomatic code.
Framework dependency
Your framework will die one day. No matter how hot it is now. I don’t have an answer for handling this. When you start a project, solutions such as micro frontends will seem over engineered when we adopt philosophies such as YAGNI. I’ve always wondered if we had developed better i-frames from the start, wouldn’t this have been a solution? To develop your application as smaller, compose-able modules that are framework independent.
DOM is imperfect. CSS is imperfect. JavaScript is imperfect. Browsers are imperfect. Frameworks try to tie these together. They don’t play well with each other. They are all or nothing. We can’t live with them, we can’t live without them. There is no silver bullet now.