When developing software, we often face problems common to all developers, and we shouldn’t try to reinvent the wheel to solve them. To achieve this, we often reach for off-the-shelf packages that can help us overcome such problems.
Packages don’t solve all our needs
Occasionally, we’ll discover a package that can accomplish 95% of what we need it to do, but either because of a bug or a slight difference in intended functionality, it doesn’t fully satisfy our requirements.
What do we do in these situations!?
By the end of this article, you will be equipped with the knowledge to solve exactly that problem.
The Problem with Third-Party Packages
When presented with a package that has a slight deficiency, we have a handful of paths we can take to make it the perfect fit for our application.
Path 1 - Direct integration into your app
We can take the ideas presented in the package and directly integrate it into code in our application. Consequently, we’ve now moved the responsibility of the task from that package and added it directly to our application. The downside to this is that it can increase complexity and technical debt.
Path 2 - Forking the package and make changes
We can fork the package directly and make the changes we need. We’ll need to maintain this new package from here on out, which can cause additional overhead than we had when we were just maintaining our application. The downside is that if we publish it to a package repository, there might be confusion about why ours is slightly different.
Path 3 - Apply a patch after package installation
We can apply a patch right after the package installation that changes what we need. The patch will be applied only to the version installed, so we’ll need to check when upgrading to make sure our patch still works or is even still valid.
This article will focus entirely on path 3
→ Applying a patch to a package.
To do that, we’re going to use the appropriately named patch-package
Understanding patch-package
Patch-package describes itself as a “vital band-aid for those of us living on the bleeding edge”. At its core, it is a utility you can install with npm or yarn that allows you to apply changes to a package immediately after that package is installed. When inserted in to your development or build steps, this becomes a very powerful feature!
See the documentation for set up instructions.
Background
To understand how to use this tool, let’s dive in to a real world example!
On a past project we found ourselves in a challenging situation: we had just invested numerous days performing some needed upgrades for a variety of packages, including a major React Native version, and after resolving many dependency issues we discovered a bug with multiline TextInput. To make matters worse, all of this occurred right before we were supposed to cut a new release build!
We began scouring the issues list on the React Native GitHub repo, and discovered one that sounded very similar to what we were experiencing. The change proposed seemed like it could resolve the issue, but we had to decide the best way to implement it:
- Use that PR’s branch as our dependency source for
react-native
- Wait days or weeks until it’s been merged
- Apply the change as a patch
We opted to reach for patch-package
to solve this issue for a few reasons: it’s succinct and understandable change and provides a timely solution to our current problem.
Using patch-package
Getting the fix in place was pretty straightforward. We followed these steps:
- Ensure that
patch-package
is installed by following the installation instructions on the package documentation. - Make the appropriate changes to implement the fix.
- In our case this means editing
node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m
- We applied the fix as indicated, changing
if (_ignoreNextTextInputCall) {
To becomeif (_ignoreNextTextInputCall && [_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
- In our case this means editing
- Run the
npx patch-package react-native
command to generate our patch
From there it was as simple as cutting a new build and testing that our fix worked. It did, we shipped it, and our client was happy!
What does a patch look like
Patches tend to be pretty small and easy to read thru, but depending on how complex your fix may be it can get a bit unwieldy.
In our case our patch is named react-native+0.71.8.patch
and looks like this:
If you looked at this and thought “That looks like a Git patch”, you’re right! patch-package
makes use of git patches to apply your changes, which makes for a very portable and universal solution.
Because at it’s core it’s just a Git patch, you could simply apply it manually via git apply
. However if you followed the installation instructions there’s a better way! By tapping in to the postinstall
hook of the package manager, these patches can be applied automatically.
Managing your patch
Because the patch is name spaced to a specific version of the package you’re using, it’s easy to manage which patches are applicable to your current setup.
For our purposes, we will keep it in place for a period of time, but when we’re ready to upgrade our react-native
package to one that supports our fix, we can just toss this patch away.
This is a huge advantage over having to use someone else’s package fork or maintain our own.
Important considerations
In our experience, a patch should be a temporary fix to a temporary problem. It’s intentional technical debt that we incur to pay off a problem. You have to make sure you keep a long term fix in mind, and move towards that solution when possible.
Additionally, there’s other great insights on the package documentation. Be sure to read it fully and understand exactly what is involved with applying a fix like this.
Conclusion
patch-package
proves time and time again to be a valuable tool for managing and maintaining fixes to our codebases effectively. By allowing our developers to make targeted fixes and modifications to third-party packages without waiting for upstream updates, we empower teams to swiftly address critical issues and implement desired enhancements.
Our developers can confidently take control of our codebase's stability and customization, fostering more resilient and adaptable software solutions.