Technology is not eternal, especially when it comes to the fleeting frontend frameworks of the javascript community. You can see below that even the most popular frontend tools have a rise and fall, some more dramatic than others.
Each generation builds on the foundations and work of previous generations. When this happens, no matter how time-tested a technology might be, it can and will reach the end of its life. If you work on web applications, have you ever been impacted by the death of significant dependency or the end of a framework’s life? React.js is still riding high in the chart above, though it may have begun to experience a downturn. Are you ready for the death of React.js?
The stream will cease to flow;
The wind will cease to blow;
The clouds will cease to fleet;
The heart will cease to beat;
For all things must die.1
This poem may be too dramatic for the occasion, but it is meant to highlight a reality. Every dependency will eventually be deprecated. Regardless of the type of technology, the scope of its usage, and the benefits it has provided, there are limited options when a technology’s end-of-life is on the horizon. Let’s consider those below.
What happens when a dependency dies?
I have migrated multiple applications from one framework to another, and limited options are available when your dependency is no longer maintained. This is incredibly challenging when that dependency is as foundational as a frontend framework. You could do nothing, but this introduces a host of problems that are usually unacceptable such as:
Security vulnerabilities will not be patched
Breaking changes may prevent future upgrades
Fewer developers will want to work using “dead” tools
Related dependencies may also die, creating even more issues
These risks require a mitigation strategy. I’ve considered multiple as I’ve participated in and led migrations from older, less popular frameworks (e.g., backbone.js) and those that have officially lost all support. (eg. angular.js). There are four main options to consider though all come with costs.
1. In-place migration
While this might seem the most straightforward answer, most applications have been written over many years with very little planning for the end of a technology’s life, especially regarding a frontend framework. Usually, code is tightly coupled, leaving migration paths risky and prone to bugs. It can be done, but it is generally preferable to rewrite the application as the cost is unpredictable, and migrations are known to draw on and on if the plan is not coordinated, well-scoped, and controlled. Preventing interoperability between new and old code is essential. If normal development continues on the application, everyone must follow the plan, or a migration project may never end. Untangling the mess of code migration is often not worth the trouble or the cost.
2. Rewrite on a new foundation
A rewrite might seem just as expensive, and in some ways, it is a type of migration, but rebuilding a house is easier when people aren’t living in it. Like a migration, a rewrite can be iterative, but rewrites build new foundations. The best course of action with a rewrite is to ensure solid end-to-end testing is in place. Product audits are essential and an excellent opportunity to “clean house” if certain features are not worth maintaining. The rewritten scopes should pass the identical end-to-end test suites. While a migration tries to decouple and use existing code paths, a rewrite creates entirely new ones. You might copy some code from previous locations, but there is no chance of continuing to couple new code to old dependencies if they aren’t found or allowed in the new location.
3. Fork & Maintain
Depending on the size and complexity of a dependency, you might consider forking the project and then maintaining and upgrading it as needed. This is usually not a good long-term option because of the developer cost required to understand and maintain an outdated tool. The end-of-life tool was also retired for a reason, and keeping it alive and working is asking for trouble in most scenarios, even if you can afford it. Most companies wouldn’t have the specialized engineers or headcount to keep something like a frontend framework alive.
4. Outsource the Upkeep
Lastly, while you might not be able to keep it alive, there are sometimes options to contract others to extend the life of a dependency. This is also usually prohibitively expensive and can be likened to COBRA insurance in healthcare. You may need it in an emergency while you find a better option, like migrating or rewriting, but this is not a long-term solution as the problems are just being delayed and at a tremendous financial cost. However, this is usually a better and safer temporary option than forking and trying to do it yourself.
Planning for dependency end of life
The reality of dependency death or deprecation is not often considered at the beginning of adoption. Usually, the most pressing problems win out, not those 10-15 years in the future, but tech companies can and have faced these scenarios already. For example, many web applications are still coping with the pain caused by the end-of-life of angular.js. This begs the question, what can be done to plan for the end? Just like planning for one’s own death, or the death of a loved one, the estate should be managed, and affairs should be put in order, but planning early can often prevent pain. Like any loss, pain is unavoidable, but there are some practical ways to ease the burden on those who follow, whether or not we are still around to see it.
Write the will before the terminal diagnosis
Writing a will is a good metaphor for understanding how to plan for the death of a dependency. A will outlines a plan for each scope of ownership. It spells out the scopes. It specifies the new owners. It tries to avoid ambiguity. As we choose to purchase or invest in new technology, future owners aren’t often on our minds. If we consider the scopes this new technology owns and possible new owners, this can make the handoff smoother in the future. A good first step is outlining each scope of ownership and any challenges that will occur when that ownership needs to be transferred. This may expose opportunities to transfer ownership early or reduce debt to avoid pain during the transfer. In the case of react.js, this could mean outlining the high-level scopes. Perhaps you only use react.js, as originally built, to render HTML views. You likely have introduced hooks (many relationships) and might even use React to manage significant amounts of application state with the Context API. You may also need to consider each peer dependency relationship like react-router, react-redux, etc. All these relationships make end-of-life even more challenging.
Avoid unnecessary relationships
The more relationships in a person’s life, the more difficult it is to write and execute their will. The same is true with any dependency we add to our applications. Each reference to that dependency and each peer dependency establishes a coupling that eventually needs to be removed or replaced. A few tips may help avoid the often massive work of decoupling that must happen at end-of-life.
Keep references to a minimum
While this may seem straightforward, when it comes to your code, you need to be ever-vigilant in reducing the number of dependencies and keeping references to those dependencies consolidated and controlled. The more you import or reference a dependency in your code, the more difficult it will be to remove it later. This means adding abstractions when possible so that you maintain the interface contracts instead of using a dependency directly. The less the dependency is directly invested in your code, the easier it will be to refactor and replace.
A diagnostic tool for understanding how much you may owe to various dependencies is easily found in your testing expectations. The more you mock a dependency in your unit tests, the more you can count on that dependency creating havoc at the end of its life. I tend to prefer patterns that follow functional patterns or dependency injection. Mocking in tests is a symptom of technical debt that eventually must be paid when a dependency dies.
I once took a course on Frontend Masters on angular.js by Scott Moss, but much of the code in the course was written so that it could be tested without angular and the traditional bloat of angular.js test runners. If your unit tests don’t require your frontend framework, you can bet it will be easier to reuse that logic in a future application when needed.
Invest in tools that promote modular design
Some tools promote modular design, and some do not. For example, react hooks will cause great pain when many apps eventually need to migrate away from React. This is because each imported hook introduces a side effect. This is true even in the most consolidated and well-written custom hooks. All hooks tightly couple those features and components to the react ecosystem. They will make it very difficult to remove.
An app that uses redux, react-redux, and its more dated connect function is a good example of decoupled and modular code. (Using react-redux’s useSelector and useDispatch does not promote modular decoupled code) When used correctly and not immediately invoked, the connect function creates reusable higher-order components with shared application state and actions. It may feel like a pipe dream for most modern react apps, but react connected to redux following this pattern will be much easier to replace with another rendering tool when the time comes. It may be an unpopular opinion, but decoupled state management libraries like redux will endure longer than the state management of many current frontend tools because they are not coupled to the browser or its ever-changing API. Most popular frontend frameworks and tools are written without an escape plan.
Consult the heirs, even if they aren’t here yet
Estate planning is not a solo project, and while we can and should plan for the death of our dependencies, we shouldn’t do it alone, and we shouldn’t expect to do it perfectly. I recommend starting a technology capabilities plan (TCP) at your company if one doesn’t exist. This outlines the current decisions and practices around current dependencies. It should also include estimates for future usage and plans to document best practices to limit those dependency touchpoints. This TCP will not cover or prevent all technical debt, but starting and continuing the conversation will leave everyone better prepared. Often, taking on technical debt is the best investment a company can make, but all debt needs to be paid, and when a death occurs, that payment becomes forced. Don’t be caught off guard when your dependency dies.
https://allpoetry.com/All-Things-will-Die