Add vector tiles to a React app: production guide

This guide explains how to add a production-ready vector-tile layer to a React application using MapLibre GL JS through the @vis.gl/react-maplibre package. It covers dependency installation, CSS, TileJSON or tile URL configuration, source-layer naming, React state management, feature interaction, deployment constraints, and operational checks.

Vector tiles let a web map request only the geographic data needed for the current viewport and zoom level instead of downloading one large geographic file. In a React application, the most important production decisions are not limited to rendering a map: you must verify the tile format, source-layer names, URL scheme, style dependencies, browser access headers, attribution requirements, and how map state is synchronized with React. This guide uses MapLibre GL JS with the React bindings maintained by vis.gl. MapLibre GL JS renders vector tiles in the browser with WebGL, while the React wrapper exposes the map as a component that can be controlled through props and hooks. (maplibre.org)

How EDK fits

For: Frontend engineers, GIS developers, and application teams building a React map that consumes hosted Mapbox Vector Tiles, MapLibre Vector Tiles, or a TileJSON endpoint. It assumes familiarity with React, npm, TypeScript or JavaScript, and basic concepts such as longitude, latitude, zoom levels, vector-tile sources, and style layers.

Adding vector tiles to React is a focused example of turning physical-world data into an interactive computational surface. The browser is not merely displaying an image: it is requesting spatially bounded data, selecting source layers, applying filters and style expressions, and exposing individual geographic features for interaction. That same pattern scales toward a world model in which roads, buildings, waterways, public assets, infrastructure, environmental features, and other physical entities can be found, compared, filtered, and analyzed through a common geographic interface. The connection is genuine but limited: this guide explains the rendering and application-integration layer, not a particular global dataset or an E2 implementation.

  • •Render a focused layer of physical-world features—such as roads, streams, buildings, parks, or public assets—while retaining the tile source's geographic partitioning and zoom-dependent detail.
  • •Use feature selection, map bounds, zoom, and source-layer filters as the front end of a larger geographic workflow that ranks, compares, measures, or aggregates real-world features by location.
  • •Combine multiple tiled layers in a React interface so users can inspect relationships between infrastructure, transportation networks, environmental systems, and service or public-data observations without converting every dataset into one monolithic browser payload.
Primary renderer
MapLibre GL JS, a WebGL-based browser renderer for interactive maps from vector tiles. (maplibre.org)
React integration
@vis.gl/react-maplibre, the React component and hook layer for MapLibre GL JS. (visgl.github.io)
Accepted vector formats
MapLibre Tile format or Mapbox Vector Tile format, as documented by the MapLibre source specification. (maplibre.org)
Required style concept
A vector source must be referenced by one or more style layers, and vector-backed layers require source-layer. (maplibre.org)

The EDK outcome

Before you start

A React project with a working development and production build.

Node.js LTS and npm or another supported package manager.

A vector-tile service, TileJSON endpoint, or complete MapLibre style document.

The provider's documented source-layer names, attribution requirements, access rules, and request configuration.

A basic understanding of longitude, latitude, zoom, GeoJSON versus vector tiles, and MapLibre style layers.

Learning objectives

Install MapLibre GL JS and its React bindings correctly.

Distinguish a vector source ID from an internal source-layer name.

Add a TileJSON or URL-template source to a React map.

Style roads, streams, buildings, or other physical features with appropriate layer types.

Diagnose invisible layers, failed tile requests, CORS errors, incorrect zoom behavior, and React/map state mismatches.

Prepare a vector-tile map for deployment with attribution, error handling, caching, and security checks.

Verification

The map container has a non-zero size at first render and after responsive layout changes.

The style document loads successfully.

The vector source returns TileJSON or vector-tile responses with successful status codes.

The source-layer value exactly matches the layer name inside the tileset.

The selected layer is visible at the tested zoom levels and is not hidden by a filter or layer order issue.

Tile, style, glyph, sprite, and icon requests work from the production origin.

Required attribution is visible and legible.

Map errors are captured in development and production diagnostics.

The map remains usable during slow, failed, or interrupted tile requests.

React state and map camera state do not diverge because of uncontrolled imperative updates.

Troubleshooting

Blank map: inspect the browser console and network panel first. Confirm the style URL, CSS import, container height, and WebGL availability.

Tiles return 200 but nothing appears: verify source-layer, geometry-compatible layer type, layer visibility, filter expressions, source bounds, and zoom constraints.

404 or malformed tile requests: compare the provider's documented URL template with the actual z/x/y requests and verify whether the server expects XYZ or TMS addressing. (maplibre.org)

CORS errors: configure the tile and supporting resource servers for the deployed application origin, or use the provider's documented proxy or signed-request pattern.

Features are cut off at tile edges: inspect the tileset's buffer and generalization settings. MapLibre documents that features render within their originating tile and that insufficient buffering can cause artifacts for wide lines, large circles, or offsets. (maplibre.org)

Map looks correct locally but fails after deployment: check HTTPS mixed-content restrictions, Content Security Policy, worker loading, CDN rewrites, authentication headers, and whether the production hostname is authorized.

Map becomes slow with many layers: reduce unnecessary layers, avoid extremely broad interactive queries, simplify style expressions, constrain layer visibility by zoom, and measure tile size and request count on representative devices.

React updates cause flicker or repeated source work: keep source and layer definitions stable, avoid recreating large arrays on every render, and prefer the wrapper's reactive props over direct native-map mutations when both can accomplish the same task. (visgl.github.io)

Next steps

Add a feature-selection workflow using an interactive layer ID and a detail panel populated from a trusted feature endpoint.

Add map-scale filters so dense physical-world layers become legible as users zoom from region to neighborhood to site.

Add explicit attribution and a data-source panel describing update frequency, coverage, coordinate assumptions, and known limitations.

Measure tile request volume, payload size, time to first rendered map, and interaction latency on representative devices and network conditions.

For broader geographic applications, define a consistent source schema so roads, waterways, buildings, public assets, environmental features, and operational observations can be searched and compared through the same map interaction model.

When the application needs measurement, ranking, aggregation, routing, or change analysis, keep those computations in an appropriate geographic service rather than assuming the display tiles contain all required precision or attributes.

What you need before writing React code

First identify what your tile provider actually publishes. A vector source may be exposed through a TileJSON URL, through one or more URL templates such as https://tiles.example.com/{z}/{x}/{y}.pbf, or through a complete style document that already declares sources and layers. MapLibre supports vector sources using MapLibre Tile or Mapbox Vector Tile formats. A TileJSON endpoint can carry metadata such as bounds, minimum and maximum zoom, and tile URL templates. (maplibre.org)

You also need the exact source-layer name inside the tile. A tileset can contain several thematic layers—such as roads, waterways, landuse, or buildings—and the source ID used in your application is not necessarily the same as the internal source-layer name. Obtain this value from the provider's documentation or inspect the tileset with a compatible vector-tile inspection tool before styling it.

  • •A React application created with a supported Node.js LTS release.
  • •The @vis.gl/react-maplibre and maplibre-gl packages.
  • •A valid MapLibre style URL or a style object.
  • •A vector-tile endpoint or TileJSON URL.
  • •The source-layer name for each layer you intend to render.
  • •A documented attribution string and any required access token or request headers.

Step 1: install the map and React bindings

The current react-maplibre getting-started documentation installs the React bindings together with MapLibre GL JS. It also requires the MapLibre stylesheet because controls, markers, popups, and other map UI elements depend on it. (visgl.github.io)

  • •npm install @vis.gl/react-maplibre maplibre-gl
  • •If the project uses TypeScript, keep the package versions aligned and run the project's normal type-check command after installation.
  • •Do not install a Mapbox GL package unless your application intentionally targets Mapbox GL JS; the MapLibre React path uses MapLibre GL JS as its base library. (visgl.github.io)

Step 2: render a map with a style

Start with a known-good style before adding your own source. A style is a JSON document that defines the map's sources, layers, drawing order, and visual properties. The MapLibre Style Specification treats sources and layers as separate objects: a source identifies data, while a layer gives that data a visual representation. (maplibre.org)

  • •import {Map} from '@vis.gl/react-maplibre';
  • •import 'maplibre-gl/dist/maplibre-gl.css';
  • •export default function App() {
  • • return (
  • • <Map
  • • initialViewState={{
  • • longitude: -122.4,
  • • latitude: 37.8,
  • • zoom: 10
  • • }}
  • • mapStyle="https://example.com/styles/base.json"
  • • style={{width: '100%', height: '480px'}}
  • • onError={(event) => console.error('Map error', event.error)}
  • • />
  • • );
  • •}
  • •A map container must have a usable width and height. A frequent integration failure is a correctly initialized map inside a parent whose height collapses to zero. Treat the map's dimensions as part of the component contract, not as an afterthought.

Step 3: add the vector source and a style layer

There are two normal ways to add the tiles. If the provider supplies TileJSON, use the url property. If it supplies a tile template, use the tiles array and include any documented minzoom, maxzoom, bounds, scheme, and attribution values. MapLibre's vector-source specification supports both forms. (maplibre.org)

  • •import {Map, Source, Layer} from '@vis.gl/react-maplibre';
  • •import type {FillLayer, LineLayer} from '@vis.gl/react-maplibre';
  • •const roadsLayer: LineLayer = {
  • • id: 'roads-line',
  • • type: 'line',
  • • source: 'local-tiles',
  • • 'source-layer': 'roads',
  • • paint: {
  • • 'line-color': '#2563eb',
  • • 'line-width': ['interpolate', ['linear'], ['zoom'], 8, 0.5, 14, 3]
  • • }
  • •};
  • •export default function MapView() {
  • • return (
  • • <Map
  • • initialViewState={{longitude: -122.4, latitude: 37.8, zoom: 10}}
  • • mapStyle="https://example.com/styles/base.json"
  • • style={{width: '100%', height: '600px'}}
  • • >
  • • <Source
  • • id="local-tiles"
  • • type="vector"
  • • url="https://tiles.example.com/roads/tiles.json"
  • • />
  • • <Layer {...roadsLayer} />
  • • </Map>
  • • );
  • •}
  • •The key fields are source: 'local-tiles', which references the source ID declared in React, and source-layer: 'roads', which identifies the internal layer inside each vector tile. For a polygon dataset, use a fill layer; for linear features such as roads or streams, use a line layer; and for point-like features, use a circle or symbol layer. The layer type and paint properties should match the geometry and intended map scale. (maplibre.org)

Step 4: make the component production-aware

Keep stable source and layer objects outside the component or memoize them when they are expensive to recreate. The React bindings perform style diffing when layer props change, and their documentation notes that avoiding newly created constant objects and arrays can help performance. (visgl.github.io)

Use React state for application-level concerns such as the selected feature, active category, current filters, and whether a detail panel is open. Use the map ref or map hooks for map-specific operations such as fitting to bounds or flying to a selected location. The wrapper provides a MapRef and a useMap hook for these cases. (visgl.github.io)

  • •Keep a stable layer definition rather than constructing a large object on every render.
  • •Use filters in the style layer for visual selection when the complete dataset remains useful in the map.
  • •Use a separate API query for authoritative feature details if the tile properties are intentionally generalized or abbreviated.
  • •Handle onError and log the failing URL, source ID, and application route in your observability system.
  • •Use onIdle or source-data events for diagnostics, not as a substitute for server-side data validation. (visgl.github.io)

Step 5: validate the delivery path before launch

Open browser developer tools and inspect the network requests while panning and zooming. Confirm that the application requests the expected z/x/y tiles, receives a successful status, and receives a content type and payload that the renderer can decode. A successful style request does not prove that the vector source is valid.

Test at several zoom levels, including below the source's minimum zoom, above its maximum zoom, and near the edges of its published bounds. MapLibre can reuse data from the maximum available zoom when displaying at higher zoom levels, but the visual result depends on how the tileset was generated and generalized. (maplibre.org)

  • •Verify HTTPS and CORS for style, tile, glyph, sprite, and icon requests.
  • •Verify required attribution is visible wherever the map is shown.
  • •Check that the tile server returns cache headers appropriate for the update frequency of the data.
  • •Test slow networks and interrupted requests; a map should fail visibly and recover gracefully.
  • •Test touch, keyboard, resize, and high-density displays if the map is customer-facing.

Workflow

01

Install dependencies

Install the React bindings and MapLibre GL JS, then import the MapLibre stylesheet from the application entry point or map component.

npm install @vis.gl/react-maplibre maplibre-gl
02

Create a map container with explicit dimensions

Render a Map component with an initial camera and a style URL or style object. Ensure the map element and its parents have non-zero width and height.

import {Map} from '@vis.gl/react-maplibre';
import 'maplibre-gl/dist/maplibre-gl.css';

export function MapShell() {
  return (
    <Map
      initialViewState={{longitude: -122.4, latitude: 37.8, zoom: 10}}
      mapStyle="https://example.com/styles/base.json"
      style={{width: '100%', height: 600}}
    />
  );
}
03

Declare the vector source

Use a TileJSON URL when available. Otherwise use the provider's tile URL template and configure metadata such as bounds, minzoom, maxzoom, scheme, and attribution as documented by the provider.

<Source
  id="transport"
  type="vector"
  url="https://tiles.example.com/transport/tiles.json"
/>
04

Add a layer using the exact source-layer name

Reference the source ID from the previous step and set source-layer to the internal layer name published by the tileset. Choose a layer type that matches the geometry.

const roads: LineLayer = {
  id: 'transport-roads',
  type: 'line',
  source: 'transport',
  'source-layer': 'roads',
  paint: {
    'line-color': '#2563eb',
    'line-width': ['interpolate', ['linear'], ['zoom'], 8, 0.5, 14, 3]
  }
};

<Map ...>
  <Source id="transport" type="vector" url="https://tiles.example.com/transport/tiles.json" />
  <Layer {...roads} />
</Map>
05

Add interaction and observability

Use the React event props and hooks for application behavior. Log map errors, test source loading, and use a MapRef or useMap when an imperative operation such as flyTo or fitBounds is genuinely required.

<Map
  ...
  onError={(event) => {
    console.error('MapLibre error', event.error);
  }}
  onIdle={() => {
    console.debug('Map is idle and visible resources have settled');
  }}
/>
06

Test the deployed request path

Inspect network requests at multiple zoom levels and locations. Confirm that every required resource is reachable from the deployed origin and that attribution, caching, authentication, and error handling work outside the local development environment.

# Inspect installed versions and dependency resolution
npm ls @vis.gl/react-maplibre maplibre-gl

# Run the production build using your project's normal script
npm run build

Questions teams ask

Why does the map load but my vector layer is invisible?

Check the source ID, the source-layer value, the layer's geometry type, and its zoom range. The source ID is the name you assign in the application, while source-layer must match the internal layer name in the vector tile. Also verify that the style layer is not hidden by a filter, placed beneath an opaque layer, or outside the source bounds. Vector-tile layers require source-layer in the MapLibre style specification. (maplibre.org)

Should I use a TileJSON URL or a tile URL template?

Use TileJSON when the provider supplies it because it can package the tile URL and metadata such as bounds, zoom limits, scheme, and attribution. Use a tile URL template when that is the provider's documented interface, and explicitly configure any required metadata in the vector source. Both patterns are supported by the MapLibre vector-source specification. (maplibre.org)

Can I fetch vector tiles directly from a different domain?

Usually, the tile, style, glyph, sprite, and icon servers must be configured for browser access, including appropriate CORS behavior. Confirm this with the provider and test the deployed origin rather than only localhost. If the provider requires a token or signed URL, keep credential handling consistent with its terms and security guidance.

Where should feature details and search results come from?

Use tile properties for lightweight map interaction when they contain the fields you need. For authoritative details, complex filtering, full-text search, exact geometry, or computations that exceed the tile schema, use a separate feature or analytical service. Tiles are spatially partitioned display inputs, not necessarily the complete source of truth.

How do I handle strict Content Security Policy rules?

Review the worker, style, tile, font, sprite, and image origins required by the application. The react-maplibre API documents a workerUrl option for cases where the default worker-loading approach is incompatible with a strict policy. (visgl.github.io)

Keep exploring

Sources

More in Guides