How to choose software libraries (with React examples)

February 24, 2023

npm laptop

A good skill to have when you're transitioning from a junior developer onto a more senior level is to know how to pick good libraries without risking being locked in.

And yes, there is about a 50% chance you'll get asked a question like "how do you choose your libraries" in a technical interview for your job. So, what would Jaka answer?

Do I need a library to do this?

During my career I've seen people, that have bad experience moving away from libraries that they've used, having to refactor half of their codebase. These people will always tell you the same thing: don't use any libraries. While most likely basing their application on some framework on which they heavily rely on. Jokes aside, I can heavily relate to the pain when having to refactor 4000 lines of code just to get rid of some library that has suddenly become exploitable.

So, should I never use a library for anything?

Well, I can only give you my humble opinion on the matter. My initial question is:

  1. Is the functionality I need from this library something very simple that I can quickly write and test?

If yes, then most likely I would not install the library.

Then, if I see that I would spent a significant amount of time implementing the logic, I ask myself:

  1. Would I be reinventing the wheel if I do this and most likely end up doing the job worse than what the library would be able to do?

If the answer is yes, then I would advocate in favor of using the library. Unless the library is closed source or has some insane telemetry going on, etc.

The basic rule-of-thumb approach

When choosing which library to use in your project, it's also a good practice to follow a few guidelines:

  1. Make sure the library has a decent amount of stars on GitHub.
  2. Make sure the library does not have many unresolved issues on GitHub (or any other project tracking software).
  3. Make sure that the library is being actively maintained.

If a library passes these few checks and suits you well, you'll probably be fine if you decide on using it.

Don't make this mistake

So, now we've decided we want to use a library for something. Yippy.

I install the library and just start using it everywhere in my codebase, right?

Hell no. Remember the guy from the beginning of this post that told you never to use libraries, because you'll have to rewrite the full application at some point just to get rid of one stupid library?

That guy has a point. But let's dive deeper. How can we use a library without directly using a library? We wrap it into an adapter.

Preparing the safety equipment

We will borrow some principles from the SOLID architecture. We will need some interface adapters to save ourselves from having to create pull requests that look like this:

a massive pull request

Let's first check out what not to do. The examples will provide React code, but these principles apply to any application in any language.

Using a library directly
import shortid from 'shortid';
	  		
export default function App() {
  return <h1>{shortid.generate()}</h1>
}

Using the library directly means that we are importing the library directly into the component files and using the functionalities directly.

Why is this not good? Imagine that our usage of shortid is present in 100 components across the codebase. The shortid library then decides to change their usage api due to a potential exploit. Now you need to call shortid.new() instead of shortid.generate(). The only way to fix this is to change the usage of the library 100 times.

This example is intentionally stupid simple, but imagine that we're dealing with a bigger library with more functions and more parameters that can change through time.

By creating an interface adapter for the utility, we have full control over how the library behaves in one single place. How do we create an adapter?

Adding an adapter
import shortid from 'shortid';
	  		
export function generateShortId() {
  return shortid.generate();
}

We create an additional function that wraps the external library's function where we have control over its parameters in one single place.

Imagine that hypothetical scenario happens, and we need to change the usage. The only thing that is different is the content of the adapter.

Using the adapter to update the library
import shortid from 'shortid';
	  		
export function generateShortId() {
	 /* We could just use the new way 
	 * of calling the library in one place
	 * instead of a 100 times
	 */
	 // return shortid.new();
	 return shortid.generate(); // here so that it compiles
}

Since we have to change the call to the library, we can do that in one place.

What about if the function were to return an object with an id property instead of just a string? We can also transform the new return type from the library into something we expect across the whole codebase by only changing the functionality in the adapter.

Using the adapter to adapt to the new return type
import shortid from 'shortid';
export function generateShortId() {
	 /* We can extract the property
	 * and return the string, as the
	 * function did before
	 */
	 // const result = shortid.new();
	 // return result.id;
	 return shortid.generate(); // here so that it compiles
}

Because we created an additional function that wraps the external library's function, we have control over its return type in one place as well.

On top of this (if we're using a statically-typed language such as TypeScript) we should make sure that the parameter and return type interfaces are strictly imposed by ourselves, so that a potential breaking change in a library doesn't make us have to call our adapter differently.

Using the adapter to adapt to the new return type
import shortid from 'shortid';
interface ShortIdParameters {
	 seed?: number;
}
type ShortIdReturn = string;
export function generateShortId({ seed }: ShortIdParameters = {}): ShortIdReturn {
	 if (seed) {
	 	shortid.seed(seed);
	 }
	 return shortid.generate(); // here so that it compiles
}

No matter what the parameter and return types of the short id generate function are, the interface for using our function will always remain the same.

Are we there yet?

We've presented a very simple idea about how you can use an adapter to transform the parameters and it's return type. But ... what if things are not so simple and the changes can potentially be huge? What if instead of just calling one function, we'd have to change the whole flow (meaning a few consecutive calls that have to be called in the correct order)?

Then, simply adapting the function to fit into a predefined interface combo won't cut it. This whole blog post is a lie!

Before you completely give up on using libraries, I'd like to point out one thing.

There is one magical thing called tests, that you should have living in your cozy codebase. If our functionalities are supported with integration and end-to-end tests, we can still potentially scrap the current library and try another one with different flows (or even write our custom logic), while still knowing whether something works as it has worked before. Our test flows will pass, regardless if we change out the logic of how certain things are accomplished under the hood.

Outro

Don't be a fool, wrap your tool. Turns out, that holds for the tooling inside of your codebase as well. 😉