DEV Community

lionel-rowe
lionel-rowe

Posted on

What makes a package useless, or "When should I reinvent the wheel"?

This is a response to a couple of comments in the discussion below this article:

@ansile wrote:

array-includes is a polyfill, first published 7 years ago.
array-flatten is kinda the same. It's not a polyfill, in a technical sense, but Array.prototype.flat is a new feature and the package pre-dates it.

Arguably, that makes array-flatten "useless" for new projects, because it's better to polyfill a standard API, as you can remove the dependency later on if you stop supporting legacy browsers. You can't do this if you're using a non-standard API.

Of course, that doesn't mean the package was useless when it was written, nor even that older projects should instantly switch to the standard API. Any such change requires some amount of additional work, so there's a trade-off between rework and technical debt.

ismobilejs is a device os/type/etc detector. Some apps definitely need that.

And store2 is a wrapper with a huge amount of features, some convenience-based (e.g. modifying currently stored value instead of manually doing get+set), and some unique (e.g. checking how many space is left).

Difficult to argue with either of these. Both of these packages fulfill usefull purposes, and reinventing the wheel here not only takes additional work but also won't be as well battle-tested as a public, open-source package with thousands of GitHub stars.

The main reasons not to use these would be:

  1. Preference for a different package that covers the same use case. For example, instead of a wrapper for the somewhat outdated localStorage and sessionStorage APIs such as store2, you could use the excellent idb-keyval, which wraps the IndexedDB API instead, giving significant performance and other advantages.

  2. Reduce bundle size by using a custom yet very simple, small, yet hacky alternative. For example, maybe you don't need all of ismobilejs's features. Per MDN's recommendation, if you simply need to know whether a UA is mobile and don't care about other details, you can get a pretty damn good approximation with just 1 line:

export const isMobile = navigator.userAgent.includes('Mobi')
Enter fullscreen mode Exit fullscreen mode

Meanwhile, @mcmath argues:

I’m going to go out on a limb and say that none of these are useless. Take upper-case, for example. I imagine it’s supposed to be useless because we already have toUpperCase(). But sometimes we want to do this:

 let upperCaseStrings =
   lowerCaseStrings.map(upperCase);

... Yes, it's easy to implement yourself. In fact, I have (many times). Maybe I'll download upper-case next time instead.

I'd argue that importing the upper-case package for this purpose would be a huge mistake and lead to increased tech debt for virtually no benefit. For extremely simple features such as this, even if you frequently need a map-able version, it'd be much better to have a custom file somewhere in your own codebase, rather than an external dependency. Perhaps it'd be called something like /src/utils/string-formats.ts and look something like this:

export const upperCase = (str: string) => str.toUpperCase()
export const lowerCase = (str: string) => str.toLowerCase()
// ...
Enter fullscreen mode Exit fullscreen mode

Importing an external package for such simple features would be a mistake, for a few reasons:

  1. Many developers will assume the package is doing something special and unique, rather than just calling String#toUpperCase(). They'll end up wasting time poring over GitHub repos, trying to figure out why someone has bothered to include this package as a dependency.

  2. Meanwhile, other developers will just ignore it and treat the package as a "black box". They won't be quite certain what it does, but they'll assume it does something vaguely similar to String#toUpperCase(). Instead of a standardized, tried-and-tested, well-known, painstakingly specified, well-documented API, they'll be left wondering. Does it work on Greek or Cyrillic text? Is it locale-sensitive, and if so, does that mean it may have different results in different user agents? Is calling upperCase(lowerCase(upperCase(str))) always identical to calling upperCase(str) for every possible value of str? Who freakin' knows!

  3. Breaking changes might be introduced to the package, which would never (or very rarely) happen with native web platform features. In general, you want to keep packages up to date, for security reasons if nothing else; but you also don't want your project to break thanks to the updates.


In general, I'd suggest the following heuristics in determining when to use a third-party package or when to "reinvent the wheel":

  • Can I implement the feature myself trivially and reliably?

  • How well is this functionality supported by existing Web (or Node) APIs?

  • Will updates to this package typically be an advantage or a disadvantage?

  • Is this a critical part of the app for which I want to be sure the solution is robust and battle-tested?

  • Does the benefit the package brings justify the increased bundle size?

Would you agree with these heuristics? What other ones would you add/remove?

Discussion (8)

Collapse
miketalbot profile image
Mike Talbot

I think those are good heuristics. I think dependency creep is an issue and you soon end up with 1000s of sub-deps etc.

There is an interesting thing from that other post though, many of those libraries are related to each other. I can see me thinking isNumber is a problem - let me write a function to put that to bed. Right now I've got that I'll just keep using it because I trust it. I want to use it in multiple projects so I publish it as an npm module. Granted it's weird not to have a package that also sorts out isEven and isOdd without recourse to it.

If the person who wrote isNumber writes enough libraries (which they have) that use their own modules that they know and trust you end up with isNumber included in the weirdest things due to sub deps.

So I guess my point is "how far do you go?" Do you look at a package which you do need and say "I won't take it, because it relies on another I don't like" etc.

Collapse
lionelrowe profile image
lionel-rowe Author

Personally I wouldn't take it that far. I'd look at the API surface of the package, the increase in bundle size, and whether it or any of its dependencies have known vulnerabilities (which NPM already automates with npm audit etc). I'd imagine the vetting process would need to be more rigorous in industries with stringent compliance requirements, though.

Collapse
nombrekeff profile image
Keff

I would and have done, in a profesional context that is. As I've explained here.

But in short: It depends on what context this packages are intended to be used, for a quick prototype or personal project I think it's okay to use some of them. But, for a real production project I would need to evaluate the individual packages and decide.

Collapse
jmfayard profile image
Jean-Michel Fayard 🇫🇷🇩🇪🇬🇧🇪🇸🇨🇴

I wonder whether npm makes it too easy to publish a package.
In the Java world, publishing something to mavenCentral is confusing and painful ; as a result it raises the bar to which projects are motivated enough to publish something.

Collapse
fjones profile image
FJones

Absolutely. And, to an extent, workflows (used to) promote publishing your own packages for ease of access. Separate something out of your code? Why not push it straight to npm?

Curation is necessary in many ways, and hosting your own package repository is a bar usually not taken unless you reach a certain size. This has gotten better, particularly with more sensible defaults and different workflows (node generators have helped hugely), but we still see the impact.

Collapse
ksaaskil profile image
Kimmo Sääskilahti • Edited on

I totally agree about preferring to write your own code instead of installing external packages (for utility code). The beautiful thing about open-source is that you can read the source code and see how to implement it yourself, and learn something new along the way.

Collapse
nombrekeff profile image
Keff

I totally agree.

Also convenience is not always the best reason to pick up a package, I've had way to many problems for adopting a package without thinking much about it. Great, it does what I want, and it does it really nicely. But after some time I needed to remove or change packages, because they do not update as quickly as you need them, they miss some critical feature, issues take way to long to be fixed... so you're stuck 5 versions behind (version in whatever framework/context), or need to re-write some part of your system with an alternative package.

Nowadays I follow these steps before adopting a package:

  • Could I implement it well in less that 2 hours? If yes, I will.
  • Who maintains this package? Is it a company, or an individual?
  • How easy is it to maintain and update?
  • How big is it?
  • How many opened issues are there, and how fast they are solved?
  • I could go on, but I think you get the gist of it

It all depends on what the context in which you use them is, for example for a personal project I will most likely try this kinds of libraries, but in a professional project I would evaluate really thoroughly if we integrate them or not.

I talk from experience here, as I've had to spend weeks solving problems caused by premature adoption of a package/library/whatever...

Collapse
nombrekeff profile image
Keff

Could I implement it well in less that 2 hours? If yes, I will.

I know this can feel wrong, but if I spend 2 hours on this, I will have complete control over it, and will not depend on other people to update or fix bugs. In the end it makes my life easier.

Though this only applies to small packages, like the ones features in the original post. Otherwise I will research what the best package is, and check with my colleagues to see if they have not issues with it.