In a recent mapping project, Houseplants Of The World, I needed to highlight specific countries on a Mapbox vector map based on user input. The countries would be highlighted in green, and the map would pan smoothly to the selected location. In this post, I'll walk through how to do that with Mapbox GL JS and show a simple local caching strategy to avoid redundant API calls.
Setup
First, we initialize the map using the mapbox-gl
library. When the map is loaded, we add the mapbox.country-boundaries-v1
vector source.
This dataset contains boundary geometries for every country, referenced by their ISO 3166-1 alpha-3 codes (e.g., "USA", "CAN").
Official Mapbox documentation is here: Mapbox Countries v1.
mapRef.current.on('load', () => {
mapRef.current.addSource('countries', {
type: 'vector',
url: 'mapbox://mapbox.country-boundaries-v1'
});
updateMapLayer();
});
Highlighting Selected Countries
To style the map dynamically, we use a match
expression. This Mapbox style expression compares the feature's ISO country code and applies a fill color only if the code is in our data list.
const matchExpression = ['match', ['get', 'iso_3166_1_alpha_3']];
for (const row of data) {
const color = 'rgb(80, 200, 120)';
matchExpression.push(row.code, color);
}
matchExpression.push('rgba(0, 0, 0, 0)');
This effectively highlights only the countries the user has selected, while leaving others transparent.
Caching Coordinates
To reduce the API calls (which cost money), we use a simple in-memory cache stored in a useRef()
hook. I chose to use useRef()
instead of useState()
for the following reasons:
- The cache (dataCache.current) can be updated in-place.
- I don’t need to re-render the component when the cache changes.
- I just want to store and reuse data between renders.
Using useState would cause unnecessary re-renders every time the cache is updated. And to keep this map animation smooth we want as few re-renders as possible. Here is a useful comparison of the two hooks: useRef vs useState and best use cases for each.
When a country is selected, we try to find its coordinates in the cache. If not found, we fetch it from the Mapbox Geocoding API and store the result for future use.
const dataCache = useRef([]);
let itemFound = dataCache.current.find(item => item.hasOwnProperty(countryISO));
if (itemFound) {
return itemFound[countryISO];
} else {
const response = await fetch(`https://api.mapbox.com/search/geocode/v6/forward?country=${countryISO}&access_token=${accessToken}`);
const geoData = await response.json();
const coordinates = geoData.features[0].geometry.coordinates;
dataCache.current.push({ [countryISO]: coordinates });
return coordinates;
}
Flying to a Country
Once we have the coordinates, we use map.flyTo()
to animate the map to the selected location:
mapRef.current.flyTo({ center: coordinates });

Effectively highlighting a single country with Mapbox
Wrap Up
Without caching, every time a new country is selected, the app would make a fresh request to the Mapbox API. In my case, the app is a database of houseplants categorized by country of origin. Since many houseplants originate from South Africa, it's likely that the same country would be selected multiple times.
By caching the geocoded coordinates locally, we eliminate redundant API calls. This not only speeds up the map’s responsiveness but also helps avoid hitting usage limits on the Mapbox API. The caching logic is simple, but it has a big impact on performance - especially when users are frequently switching between countries.
See my full code here on GitHub: Map.jsx