Overview
Been uses Mapbox GL JS through the react-map-gl wrapper to render an interactive 3D globe that visualizes visited countries. The integration combines Mapbox’s powerful mapping capabilities with React’s component model.
Mapbox GL JS is a JavaScript library that uses WebGL to render interactive maps from vector tiles. It provides high-performance rendering with smooth animations and 3D capabilities.
Globe Component
The main map component is defined in src/components/globe.tsx:
import { useAtomValue } from 'jotai' ;
import { Map , NavigationControl , Source , Layer } from 'react-map-gl/mapbox' ;
import type { MapRef } from 'react-map-gl/mapbox' ;
import { focusAtom , selectedCountriesAtom } from '../state/atoms' ;
export const Globe = memo (
forwardRef < MapForwardedRef >(( _ , ref ) => {
const internalRef = useRef < MapRef >( null );
const selectedCountries = useAtomValue ( selectedCountriesAtom );
const focus = useAtomValue ( focusAtom );
return (
< Map
mapboxAccessToken = { apiKeyMapbox }
mapStyle = { prefersDark ? darkThemeUrl : lightThemeUrl }
antialias
minZoom = { minZoom }
ref = { internalRef }
>
< NavigationControl showCompass = { false } />
< Source
id = { MapboxSourceKeys . Countries }
type = "vector"
url = "mapbox://mapbox.country-boundaries-v1"
/>
< Layer
id = { MapboxLayerKeys . Been }
type = "fill"
source = { MapboxSourceKeys . Countries }
source-layer = "country_boundaries"
filter = { beenFilter }
paint = { beenPaint }
/>
</ Map >
);
})
);
Configuration
API Key
The Mapbox API key is loaded from environment variables:
const apiKeyMapbox = import . meta . env [ 'VITE_API_KEY_MAPBOX' ] as string | undefined ;
Required: You need a valid Mapbox access token to use the map. Get one for free at mapbox.com .
Map Styles
Been supports both light and dark themes:
const darkThemeUrl = 'mapbox://styles/mapbox/dark-v11' ;
const lightThemeUrl = 'mapbox://styles/mapbox/light-v11' ;
const prefersDark = useMatchMedia ( '(prefers-color-scheme: dark)' );
// In component:
< Map
mapStyle = {prefersDark ? darkThemeUrl : lightThemeUrl }
// ...
/>
The map automatically switches between light and dark themes based on the user’s system preferences.
Zoom Constraints
const minZoom = 1.8 ;
< Map minZoom = { minZoom } />
The minimum zoom level is set to 1.8 to ensure the entire globe is visible and prevent users from zooming out too far.
Data Sources
Mapbox data is organized into sources (data) and layers (visualization).
Country Boundaries Source
Been uses Mapbox’s built-in country boundaries dataset:
< Source
id = { MapboxSourceKeys . Countries }
type = "vector"
url = "mapbox://mapbox.country-boundaries-v1"
/>
Key details:
type="vector" - Uses vector tiles for better performance and scaling
mapbox://mapbox.country-boundaries-v1 - Mapbox’s curated country boundary dataset
Each feature includes an iso_3166_1 property matching ISO 3166 country codes
Vector tiles contain geometric data that’s rendered on the client, allowing for smooth zooming and rotation without pixelation.
Visualization Layers
Been uses two layers to visualize visited countries:
1. Country Fill Layer
Highlights visited countries with a colored fill:
const beenFilter : FillLayerSpecification [ 'filter' ] = useMemo (
() => [ 'in' , [ 'get' , 'iso_3166_1' ], [ 'literal' , selectedCountries ]],
[ selectedCountries ],
);
const beenPaint : FillLayerSpecification [ 'paint' ] = useMemo (
() => ({
'fill-color' : '#fd7e14' ,
'fill-opacity' : 0.6 ,
}),
[],
);
< Layer
id = { MapboxLayerKeys . Been }
type = "fill"
source = { MapboxSourceKeys . Countries }
source-layer = "country_boundaries"
beforeId = "national-park"
filter = { beenFilter }
paint = { beenPaint }
/>
How it works:
Filter Expression
The filter uses Mapbox’s expression syntax: [ 'in' , [ 'get' , 'iso_3166_1' ], [ 'literal' , selectedCountries ]]
['get', 'iso_3166_1'] - Gets the country code from each feature
['literal', selectedCountries] - The array of selected country codes
['in', ...] - Checks if the country code is in the selected array
Paint Properties
Only countries matching the filter are rendered with:
Orange fill color (#fd7e14)
60% opacity for a subtle effect
Layer Ordering
beforeId="national-park" ensures the layer appears below park labels but above the base map
2. Building Extrusion Layer
Adds 3D building visualizations in visited countries:
const buildingsFilter : FillExtrusionLayerSpecification [ 'filter' ] =
useMemo (() => [ '==' , 'extrude' , 'true' ], []);
const buildingsPaint : FillExtrusionLayerSpecification [ 'paint' ] =
useMemo (
() => ({
'fill-extrusion-base' : [
'interpolate' ,
[ 'linear' ],
[ 'zoom' ],
15 ,
0 ,
15.05 ,
[ 'get' , 'min_height' ],
],
'fill-extrusion-color' : [
'case' ,
[ 'in' , [ 'get' , 'iso_3166_1' ], [ 'literal' , selectedCountries ]],
'#fd7e14' ,
'#fd7e14' ,
],
'fill-extrusion-height' : [
'interpolate' ,
[ 'linear' ],
[ 'zoom' ],
15 ,
0 ,
15.05 ,
[ 'get' , 'height' ],
],
'fill-extrusion-opacity' : 0.6 ,
}),
[ selectedCountries ],
);
< Layer
id = { MapboxLayerKeys . Buildings }
type = "fill-extrusion"
source = "composite"
source-layer = "building"
minzoom = { 15 }
filter = { buildingsFilter }
paint = { buildingsPaint }
/>
3D Building Features:
Buildings only appear at zoom level 15+: This prevents performance issues and visual clutter at lower zoom levels.
Building heights interpolate smoothly as you zoom: [ 'interpolate' , [ 'linear' ], [ 'zoom' ], 15 , 0 , 15.05 , [ 'get' , 'height' ]]
At zoom 15.0: height = 0 (flat)
At zoom 15.05: height = actual building height
Creates a smooth “rising” animation
Buildings in visited countries are highlighted with the same orange color: [ 'case' ,
[ 'in' , [ 'get' , 'iso_3166_1' ], [ 'literal' , selectedCountries ]],
'#fd7e14' , // Visited countries
'#fd7e14' , // Other countries (same color in this case)
]
Map Navigation
Focus on Countries
When a user selects a country, the map automatically pans and zooms to it:
const focus = useAtomValue ( focusAtom );
useEffect (() => {
if ( ! focus ?. bounds ) {
return ;
}
const { current : map } = internalRef ;
map ?. fitBounds ( focus . bounds );
}, [ focus ]);
How it works:
When a country is selected, addCountryAtom sets the focusAtom
The Globe component’s effect hook detects the change
fitBounds() animates the map to show the country’s bounding box
The bounds are stored in the country data as [minLng, minLat, maxLng, maxLat]
User Experience: This automatic navigation helps users immediately see where their selected countries are located on the globe.
Navigation Controls
The map includes built-in navigation controls:
< NavigationControl showCompass = { false } />
This adds zoom in/out buttons. The compass is hidden since the globe projection doesn’t support rotation in the same way flat maps do.
Memoized Filters and Paint
Mapbox layer configurations are memoized to prevent unnecessary recalculations:
const beenFilter = useMemo (
() => [ 'in' , [ 'get' , 'iso_3166_1' ], [ 'literal' , selectedCountries ]],
[ selectedCountries ], // Only recalculate when selections change
);
const beenPaint = useMemo (
() => ({
'fill-color' : '#fd7e14' ,
'fill-opacity' : 0.6 ,
}),
[], // Never changes, compute once
);
Component Memoization
The entire Globe component is wrapped in memo() to prevent re-renders:
export const Globe = memo (
forwardRef < MapForwardedRef >(( _ , ref ) => {
// Component implementation
})
);
This ensures the map only re-renders when its props or consumed atoms actually change.
Ref Forwarding
The component forwards refs to expose map methods:
export interface MapForwardedRef {
isSourceLoaded : ForwardedRefFunction < MapRef [ 'isSourceLoaded' ]>;
querySourceFeatures : ForwardedRefFunction < MapRef [ 'querySourceFeatures' ]>;
}
useImperativeHandle (
ref ,
() => ({
isSourceLoaded : ( ... params : Parameters < MapRef [ 'isSourceLoaded' ]>) => {
return internalRef . current ?. isSourceLoaded ( ... params );
},
querySourceFeatures : ( ... params : Parameters < MapRef [ 'querySourceFeatures' ]>) => {
return internalRef . current ?. querySourceFeatures ( ... params );
},
}),
[],
);
This allows parent components or tests to query the map state programmatically.
State Synchronization
The map stays synchronized with application state through Jotai atoms:
const selectedCountries = useAtomValue ( selectedCountriesAtom );
const focus = useAtomValue ( focusAtom );
Data flow:
Responsive Design
The Globe component adapts to different screen sizes through CSS Grid:
// In App component
< div className = "grid size-full grid-rows-[auto,1fr,auto] md:grid-cols-3 md:grid-rows-[auto,1fr]" >
< div className = "order-2 min-h-[60vh] md:col-span-2 md:row-span-2" >
< Globe />
</ div >
</ div >
Layout behavior:
Mobile: Globe takes 60% of viewport height, menu below
Desktop: Globe spans 2/3 of screen width, menu sidebar on left
Testing Considerations
The Globe component includes a test mode:
const testMode = import . meta . env . MODE === 'test' ;
< Map testMode = { testMode } />
When testMode is enabled, react-map-gl uses a mock implementation that doesn’t require WebGL, making it possible to run tests in Node.js environments.
Mapbox GL JS Official Mapbox GL JS documentation
react-map-gl React wrapper documentation
State Management Learn how state drives the map
Architecture Overall application architecture