davidprelinger.de


Failures in Wrapping UI Components

UI wrapper components fail in two symmetric ways: the rename layer, which abstracts nothing, and the wrapper that knows too much.

· 4 min read

The typical advice for dealing with volatile, complicated or legacy third-party UI components is to wrap them in a wrapper component. The promise of putting a component between your app and the weird dependency is that you only need to make changes in one file when the vendor changes its API, or it gets replaced entirely. The blast radius stays limited - at least in theory. In practice my own wrappers have failed in one of two ways.

Let me use a hiking app as an example. Users plan trekking routes at home, then navigate them on the mountain. The map on screen comes from the Google Maps SDK, and because the team has been burned by SDK migrations before, they build a wrapper. What language does the wrapper speak?

The rename layer

The first failure mode is the one that happened to me first: a wrapper that abstracts in name only. The component exists, but every hook and object of the SDK reappears on the wrapper’s surface under a slightly different name. useGoogleMap becomes useMap. GoogleMapProps becomes MapProps, with the same fields. The wrapper’s panTo takes the arguments Google’s panTo takes, means what Google’s means, and fires its callbacks in the order Google fires them. In the worst version, the vendor’s types are simply re-exported.

No abstraction happened here. The decision the wrapper was supposed to hide, which map library we use, is legible in every one of its signatures. Callers no longer say Google, but they still think Google: they reason in Google’s zoom scale, Google’s camera model, Google’s event timing. If the team ever migrates to another SDK, like MapLibre, the migration will touch every caller anyway. The semantics leak out even though the imports do not.1

The wrapper that knows too much

The second failure mode goes wrong in the other direction. This wrapper does not resemble the SDK at all; it resembles the product. Its surface is built from the app’s own nouns: centerTrekkingRoute(routeId), showDifficultyOverlay(). Each method exists because some screen needed it and is implemented by reaching down into the SDK.

At first its methods read like requirements. But look at what the component has become. centerTrekkingRoute(routeId) takes an id, which means the wrapper now loads routes; a UI adapter has quietly acquired a repository. showDifficultyOverlay encodes a product decision about how difficulty is presented, which means the wrapper now changes when the product changes, not when the map library does. Every feature that touches the map adds a method, every team shipping such a feature edits this one file, and the component that was meant to insulate the app from Google has become a warehouse for map-adjacent business logic with an SDK call at the bottom. A nightmare.

The tell I use is the second-app test. Could a city-tour app, with the same map needs but a different domain, use this wrapper? If the wrapper’s interface mentions anything from your backlog, the answer is no.

Three vocabularies

I think both failures come down to the same decision: which vocabulary the wrapper speaks. Three languages are available for its interface. It can speak the vendor’s language: panTo, camera options, clustering config. It can speak the domain’s language: routes, waypoints, difficulty. Or it can speak the language of the thing the vendor’s product is an instance of.

So what is the Google Maps SDK a kind of? It is an interactive map - the category whose other members are Mapbox, MapLibre, Leaflet. The category has a vocabulary of its own, and it belongs to neither Google nor the hiking app: a viewport with a center and bounds, markers, paths drawn over geography, gestures, the projection between screen coordinates and world coordinates. That is the language the wrapper should speak.

Above it, the hiking app writes a TrekkingMap component that translates domain into category: it loads the route behind the id and calls drawPath and fitBounds. Below it, the adapter translates category into vendor: fitBounds becomes whatever Google’s camera API wants this year. The domain component knows nothing about Google; the adapter knows nothing about trekking.

Replace the library and only the adapter changes. Redesign the trekking experience and only the domain component changes. The two reasons to change now live in two places.

One library is not a category

There is a caveat. You cannot derive a category from a single example. A team that has only ever used Google Maps does not know which parts of the SDK’s shape belong to the category and which are Google’s. Is zoom as a number from 0 to 21 an interactive-map concept or a Google concept? Are camera animations essential to the category or specific to one vendor? Design the category interface with one library in mind and you will trace the silhouette of that library.

Before freezing the wrapper’s interface, I sketch how a second concrete library would implement it. If Leaflet could sit behind your interface instead, the interface is probably speaking category. If you cannot name a plausible second implementation, you do not yet know what the category is, and the honest move is to postpone the wrapper and let callers import the vendor for a while longer. Coupling that is visible and understood is a better position than an abstraction that is wrong.


  1. Joel Spolsky, “The Law of Leaky Abstractions”, joelonsoftware.com, 2002. ↩︎


Topics: Frontend Architecture