Definition of terms
Abstraction
Abstraction is one of the key concepts of object-oriented programming (OOP), it is the process of hiding implementation (or irrelevant) details from the user. The user is only given access to the details that are relevant.
A vehicle is an example of an entity that uses abstraction. The driver is not privy to how the parts of the vehicle are structured or arranged, or how the accelerator and engine function. The driver is only exposed to the details of the vehicle that is relevant to their driving comfort and maintenance of the vehicle. It could be quite awkward if there was no bonnet and the driver has to see how every movement affects the engine.
External libraries
External Libraries, also known as third-party dependencies, are pieces of pre-written code that are easy to implement in your code. Many times, the developer doesn't have to know exactly what is inside that code, they just need to know how to use it to enhance the app's functionality.
Engineers who build react applications are familiar with using external libraries because react is a library that allows you the flexibility of customization. Some external libraries you may have come across include react-router, Axios, sass library, material-UI etcetera...
Abstracting external libraries
Abstracting external libraries in your react applications is a design pattern that involves adding some level of abstraction between your code and external libraries.
This abstraction is typically created by wrapping the external libraries in a wrapper function and then using the wrapper function in every part of the code that requires the use of the library.
In some codebases, you would often see these wrapper functions being referred to as an adapter or a facade.
Let's examine a code sample. The react application below uses the fetch
API to make a request to the country rest API
// ./App.js
import { useEffect, useState } from "react";
import { Country, Error } from "./components";
import "./styles.css";
export default function App() {
const [country, setCountry] = useState([]);
useEffect(() => {
fetch(`https://restcountries.com/v3.1/all`)
.then((res) => res.json())
.then((data) => setCountry(data));
}, []);
return (
<div className="App">
<h1 className="App-title">Countries of the world </h1>
<div>{country ? <Country data={country} /> : <Error />}</div>
</div>
);
}
Now, the above code works fine, but imagine that the web application has over 10 pages and we used the fetch
API on every single one of those pages.
In some cases, this might be fine but imagine that your team has recently decided to migrate from using fetch API to Axios (which is also an API used to make requests to the server). Making code changes to every single one of the 10 pages (and possibly more) is not only a ton of work, but it's also very likely to introduce many bugs to your codebase.
You could convince your team to stick with the fetch API to save some arduous work, but what if the external library becomes deprecated 😲 Now you have no choice but to go through the tedious work of refactoring, and preparing yourself to fix the bugs you are likely to introduce to the codebase.
This might seem far-fetched, but the request API is an example of one of several external libraries that got deprecated, no longer supported, and unmaintained with no security patches or bug fixes. Having to refactor where you used these external libraries in your code is a lot of repetitive work (DRY)
Abstracting external libraries, therefore, means that we have created a single wrapper or instance of call for that external library and you would only have to change the external library in one instance.
Let's look at a code example where I have created a wrapper function for my fetch API
// ./wrapper/fetchAPI.js
const getData = (url, setState) => {
fetch(url)
.then((res) => res.json())
.then((data) => setState(data));
};
export { getData };
Now, I can call the getData
method instead of the fetch API directly in every part of my code. And if I need to switch over from fetch to Axios, I only need to do this in one place.
In my App.js, the getData function will be used like this 👇🏼
import { useEffect, useState } from "react";
import { Country, Error } from "./components";
import "./styles.css";
import { getData } from "./wrapper/fetchAPI";
export default function App() {
const [country, setCountry] = useState([]);
useEffect(() => {
getData(`https://restcountries.com/v3.1/all`, setCountry);
}, []);
return (
<div className="App">
<h1 className="App-title">Countries of the world </h1>
<div>{country ? <Country data={country} /> : <Error />}</div>
</div>
);
}
Find the full code here
Why you should abstract external libraries in your react applications
This has already been mentioned in earlier illustrations, but here is a bulleted list:
- External libraries can become unreliable, unmaintained or deprecated. If this happens, abstracting them means you only have to make the change to the library in one part of the code
- Decoupling: In many cases, using external libraries directly in your code leads to tight coupling
- Abstracting external libraries mitigates risks in high-impact refactoring
- Abstracting external libraries makes replacing said library much easier
Drawbacks
Like most choices in software design patterns, there are tradeoffs for using the wrapper technique:
- Extra work
- Writing documentation to guide other developers on the team and project in understanding the wrappers
So, before writing wrappers for every external library you use, you want to ask yourself some questions:
Is this library a “commodity” library? Are there other libraries that exist which provide a similar service at the same level of abstraction?
If it is, you might want to wrap it. This is because if there are a lot of other libraries with the same level of abstraction, there’s a good chance it's roughly correct, and wrapping will allow you to easily replace or extend it.
Is this library a functional library like Lodash?
If it is, it's probably not worth wrapping.