πŸ“¦ Upgrading Dependencies

NPM is a registry where humble JavaScript developers publish their libraries that other developers can install and utilize them in their projects.

While you're reading this fancy article, I've utilized React, Tailwindcss, Next.js, MDX, dayjs and multiple other libraries to help me put together the content behind it without having to solve many other problems, like how I render these content into HTML, style them, re-use logic, format dates and more.

These NPM packages are usually open-source and developed openly on platforms such as GitHub. Its possible for anyone to create such libraries to share code with others, and publish them to NPM.

JavaScript ecosystem is (in)famous of having more than one way to achieve the same thing for all most anything, its a blessing and a curse depending on the situation. But mostly a blessing since you'll not be locked to one approach or one tool.

NPM registry that holds the packages live in the cloud ☁, you can search the registry on NPM Website but to add, remove or upgrade a package in your projects, you need npm cli. Though npm is not the only CLI out there, tools like yarn and pnpm are providing same functionalities with different implementations under the hood that might make them more efficient replacement of npm.

I myself am a yarn-ist, 4 years ago I looked at yarn and never went back to npm, unless when I'm contributing to projects that use npm, otherwise for personal and professional projects yarn is the way for me. Rule of thumb is, if you see yarn.lock file in the project, use yarn, if you see package-lock.json then use npm.

Yarn berry (v2 and above) introduced a couple years ago has a wildly different approach towards how it treats package installs. Even though its in maintenance phase, classic yarn is still and excellent choice for your next project that uses JavaScript.

Adding Dependencies

Let's say I want to add latest version of the awesome recharts library to my project, I will run:

yarn add recharts

or if I want to check my code while I'm writing JavaScript, I will need this only when I'm developing but not when I deploy my app to production, so I'll run

yarn add -D eslint

If you don't need a package when your app is running in production, make sure to add it to the devDependencies using -D flag! Otherwise it adds network overhead and your app feels like a human sneezing 🀧

Adding packages one by one is time consuming, since the Package Manager must fetch many necessary information about the package, the versions, the dependencies of that dependency, and of that dependency. you can add all your main packages or all your dev dependencies in one go, at least those that are obvious when you start by running:

yarn add react react-dom next recharts

or

yarn add -D eslint prettier tailwindcss lint-staged

It might be tending to add a lot of packages to your project, but you should pay attention what comes with it. Also don't be cocky and feel you can implement everything yourself, life is too short to reinvent every wheel!

Images can be more powerful than words, here is a guide how much you should add dependencies to your project⬇

JS developer adding NPM packages to a simple todo app

Removing Dependencies

Deleting code can be satisfying, anytime you thought you need to delete a package, just run:

yarn remove recharts

You don't need to specify version of the package, or whether its main or dev dependency, it'll just work.

Now you know how to add and remove a dependency in your app, its time to learn about upgrading them.

Upgrading Dependencies

Before I can tell you about upgrading, I must briefly tell you about software semantic versioning, that NPM packages should conform to:

Semantic versioning

Each version of a package contains three segments as illustrated below:

1.7.2
β–² β–² β–²
β”‚ β”‚ β”‚
β”‚ β”‚ └── patch segment, incremented for releases with bug fixes.
β”‚ β”‚
β”‚ └──── minor segment, incremented for releases with features
β”‚       or enhancements with no breaking change.
β”‚
└────── major segment, incremented for releases that that
        aren't backward compatible with previous release.

Breaking Change is a software change in a release that makes it incompatible with the previous release. If a project depending on that software upgraded it to the newer release without changing its source code to adapt to new changes, most probably it'll crash, or worse, have negative impact on the software and the user's experince.

NOTE: When major segment of a version is 0 its for projects that are in development phase, so minor releases in this case is not guaranteed to be backwards compatible. There are projects are widely adopted and still don't have stable v1, such esbuild and react native!

What version should you install in new projects?

When you start a project its important to use latest and greatest version of all packages, unless its incompatible with another one of your package versions that is has more significance. For example if you need to use a certain UI library that is crucial for your product but its not compatible with React 18, you can use React 17 instead as an example. Though you need to think through these constraints and define a strategy for how you approach it.

What version should you upgrade to in current projects?

Current projects that are ongoing with big parts of it already built, requires more care regarding upgrades and adding dependencies. Ideally patch and minor versions of your dependencies are safe to upgrade with no changes required on your side, for example it is safe to upgrade from @remix-run/react@v1.15.0 to @remix-run/react@v1.15.1 or @remix-run/react@v1.16.0 since those upgrades must not contain any breaking changes.

When you find a breaking change in a released version with no major increment in the version, you should report it to the author, usually these are followed by another minor or patch release to revert the breaking change back to the expected behavior.

To see if a new version available for your dependencies, you can run:

yarn outdated

this will show you an output like below:

Output of 'yarn outdated' command

Patch & Minor releases

As I mentioned earlier, patch and minor releases should not include any breaking changes yet I recommend you read the release notes carefully before taking any action. Having that in mind its safe to upgrade all your dependencies that have newer patch or minor releases all-together. It might be scary to do since you'll be afraid of breaking your project, but you shouldn't worry, most of open source authors do respect semver rules. At the end we trust them with their code anyway.

Patch releases also are special, you might need to pay attention to the dependencies you're using, patched versions usually contain bug fixes, but also sometimes security fixes that can compromise your system, or worse your users.

With yarn upgrading these dependencies is really easy, yarn has an interactive command upgradeInteractive or upgrade-interactive that let's you choose one by one or all patched and minor releases of outdated dependencies and then upgrade them as you want, let's do it:

First run

yarn upgradeInteractive

This will show you a table of outdated dependencies with patch or minor new versions, like this:

output of command 'yarn upgradeInteractive'
  • Use ↑ and ↓ arrow keys to switch target packages.
  • Use SPACE key to select and de-select the current target package.
  • Use A key to select and de-select all packages listed.
  • Use ENTER key to upgrade selected packages.
  • Use CTRL+C keys to exit and not upgrade anything.

No package is selected by default when the interactive upgrade command runs, so all packages has ( ) in the left of their name, when you hit SPACE it'll be marked with a star as (*).

Major releases

One more time, major versions are usually containing a breaking change. In these new versions where the major segment is incremented, the libraries usually have some APIs added, some APIs removed and some APIs have changes in their behavior that are ultimately considered breaking due to the disrupting difference in the new behavior. Some libraries might release a major version with no breaking change on their code, but due to an upgrade of one of their dependency, though this is rare.

Upgrading to a major version takes more time and effort than other types mentioned earlier. Its called breaking change for a reason, only upgrading the dependency most probably gonna break your application, even if it still compiles or runs, you're never safe when you don't update your code.

For example, if you're using react@16.8.0, updating to react@17.0.2 might be safe in the first glance, since it had very few breaking changes, but some behaviors did change in a breaking manner, that your code might've depending on the old behavior of react.

Its important to read release notes carefully, gather information about the breaking changes and create an approach for your codebase to upgrade to the new behaviors and APIs in the new version. The release notes might be accompanied by Blog posts or deep dive articles, or pointing to GitHub issues and discussion, even better Video Blogs explaining what changed and how to upgrade. If you're working on a team, its often enough for one person to dedicate to upgrading dependencies while others working on tasks allocated to the team.

Its worth mentioning that, you don't always need to upgrade to major versions of every dependency on your project, especially projects that are done and went into maintenance phase where you fix issues but don't add new features or do big changes, you're happy, the customer is happy.

Now, let's say you want to upgrade to a the latest and greatest version of React in one of your projects, to ensure you have latest version of react you can run:

yarn add react@latest

latest is a tag, points to the latest stable version on NPM so user don't have to type version numbers.

If you're multiple major versions behind for one of your dependencies, say you're on React version 16, you want to migrate to React 18, I recommend to upgrade to the next major version first. Not every library makes this gradual upgrade smooth between multiple versions, but some really do.

To upgrade to a specific version you can use the version number after the @ symbol, like:

yarn add react@17.0.2

There are libraries that have peer dependencies, these are separate independent packages that are need for your dependency to work as expected, for example when your creating a web application with react, you need react-dom installed as well, they can be of different versions, but those packages that are part of the same repository or organization are usually must be the same version, for example to use react@18.0.0 you have to have react-dom@18.0.0, when there is a case like this documentations usually point it out. They might work on different versions but then you never know when it breaks, and its really hard to debug and find what is the issue, just keep an eye on that.

Versions

When you're performing an action on your dependencies with package managers like npm or yarn you'll separate the name of the package from the tag/version by an @ symbol.

Versions usually refer to a number like 3.0.4 while tags can be words that refer to a specific version, for example you can use latest to refer to the latest released version without knowing which version that is.

There are conventions to help you use latest version, a specific version or any version. Let me show you:

  • ^ symbol: is used to refer to the latest version in the specified major version. Compatible with specified version, in other words. for example when you add recharts@^2.1.0 to your project today that 2.1.0 is the latest version it'll install that, but in a month, when you run yarn or npm install and say recharts has released a couple versions like 2.1.1, 2.1.2, 2.2.0 and 3.0.0, then your package manager will install 2.2.0 because that is the latest release in the version 2 range, while 3.0.0 might be the latest release but its out of the version 2 range, and it most probably has a breaking change, so your package manager will install latest safe version for you.
  • no symbol: same as = sign, is used to pin a version, pinning is a strategy where you specify exactly what version you want to use and only that version, like recharts@2.0.3, which tells package managers no matter how many patch, minor or major releases have been released after specified version, just install exactly what I specified. One scenario might make this strategy useful is when you upgrade to a patch or minor version in the same range, but the author has mistakenly published some breaking changes, you can pin your dependency until next versions where the breaking change is reverted and its safe to upgrade. Another example is when you want to try a specific alpha, beta or canary release, these releases don't guarantee semver so you have to pin your version to make sure your usage of the library is consistent.

This two strategies are most popular and handy when you develop an app for users. There are other strategies too, that you might find use-cases for while developing apps or libraries. Head to node semver for a more comprehensive read.

Lockfile

When you add a package, package managers edit your package.json file, this file tells your package manager what dependencies and versions of that dependency they should add, for example when you install them on another machine, or in CI or maybe a teammate installs them, you all get same packages. As we mentioned above the package versions can have a range or specify no range, or pin the version or might ask for smaller or bigger versions. You might have guessed by now, that this is not very reproducible, for example you installed a dependency last month that had 3.0.0 version, but today your colleague runs yarn or npm i and if the package has compatible releases with version 3, they'll get newer versions, which means you get different versions, this hits hard when there is difference in behavior or changes that you didn't anticipate but your colleague does and its so hard to understand what is going on.

To mitigate this issue, package managers will generate a special file called lockfile, this file contains information about your direct dependencies, and their dependencies which are called transitive dependencies, so when you, a colleague or the CI machine wants to install the dependencies, they all get exactly the same versions regardless of the ranges specified in package.json, though there are flags package managers support to ignore the lockfile or use the lockfile even stricter.

Usually with patch and minor releases, package managers do not update the package.json, when you specified a range, they update the lockfile to what version you want for the specified dependency, again, everyone gets the same versions of the packages.

Each package manager uses different name and structure for their lockfile, and they're incompatible with each other, yarn will not use lockfile's generated by npm, so does npm and others. Yarn generates yarn.lock while npm generates package-lock.json.

Epilogue

If you're using GitHub to host your app or library repository, make sure to turn on dependabot and security advisory features, they help you update your dependencies when new versions released, this is helpful to mitigate security issues swiftly and keep your dependencies up to date. Read more on dependabot on GitHub Blog

Well that was not short was it? anyway, see you around πŸ‘‹.

npmyarndependencies