| Abdulrhman Al-Qassas

How to Build an Interactive Map with React and OpenLayers

Introduction

Maps are one of the most useful interfaces in modern web applications. They turn location data into something people can explore, compare, and understand quickly. For a React developer working with geospatial products, OpenLayers is a strong choice because it gives you control over layers, projections, vector data, markers, interactions, and external map services.

This post walks through a clean way to build an interactive map with React and OpenLayers. The goal is not only to show a map, but to structure it so it can grow into a real application.

Why OpenLayers?

OpenLayers is an open-source JavaScript library for rendering maps in the browser. It can display tile layers, vector layers, markers, GeoJSON, WMS, WMTS, and custom sources.

I like OpenLayers when the project needs more than a simple embedded map. It gives you enough control to build dashboards, location search, drawing tools, custom markers, route previews, and GIS-style interfaces.

Core Concepts

Before writing React code, it helps to understand the three main pieces.

Map

The map is the main OpenLayers object. It connects the HTML element, layers, view, and interactions together.

View

The view controls the center, zoom level, and projection. Most web maps use EPSG:3857, so coordinates usually need to be converted with fromLonLat.

Layers

Layers define what the user sees. A map can have a base tile layer, vector layers for features, marker layers, and image layers. Keeping layers separate makes the map easier to maintain.

Install OpenLayers

npm install ol

If your project uses pnpm:

pnpm add ol

Create a Reusable Map Component

The most important React rule is simple: create the OpenLayers map once, then update it intentionally. Do not recreate the map on every render.

import { useEffect, useRef } from 'react';
import Map from 'ol/Map';
import View from 'ol/View';
import TileLayer from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';
import { fromLonLat } from 'ol/proj';
import 'ol/ol.css';

export function OpenLayersMap() {
  const mapElementRef = useRef<HTMLDivElement | null>(null);
  const mapRef = useRef<Map | null>(null);

  useEffect(() => {
    if (!mapElementRef.current || mapRef.current) return;

    mapRef.current = new Map({
      target: mapElementRef.current,
      layers: [
        new TileLayer({
          source: new OSM()
        })
      ],
      view: new View({
        center: fromLonLat([31.2357, 30.0444]),
        zoom: 10
      })
    });

    return () => {
      mapRef.current?.setTarget(undefined);
      mapRef.current = null;
    };
  }, []);

  return <div ref={mapElementRef} style={{ width: '100%', height: 480 }} />;
}

This component is small, but it already has the right foundation: the DOM target is stored in a ref, the map instance is stored in a ref, and cleanup happens when the component unmounts.

Add Markers with a Vector Layer

Markers should usually live in a vector layer. That keeps marker data separate from the base map and makes it easier to add selection, hover states, clustering, and styling later.

import Feature from 'ol/Feature';
import Point from 'ol/geom/Point';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import { Circle, Fill, Stroke, Style } from 'ol/style';
import { fromLonLat } from 'ol/proj';

const marker = new Feature({
  geometry: new Point(fromLonLat([31.2357, 30.0444])),
  name: 'Cairo'
});

marker.setStyle(
  new Style({
    image: new Circle({
      radius: 7,
      fill: new Fill({ color: '#2563eb' }),
      stroke: new Stroke({ color: '#ffffff', width: 2 })
    })
  })
);

const markerLayer = new VectorLayer({
  source: new VectorSource({
    features: [marker]
  })
});

You can add this markerLayer to the layers array when creating the map, or add it later with map.addLayer(markerLayer).

Keep React State and Map State Separate

OpenLayers has its own state system. React also has state. Problems start when both try to control the same thing too often.

A good pattern is:

  • Store the map object in a ref.
  • Store UI state in React.
  • Use effects to sync deliberate changes from React into OpenLayers.
  • Listen to OpenLayers events only when you need user-driven updates.

For example, if you want React buttons to change zoom:

function zoomIn() {
  const view = mapRef.current?.getView();
  const zoom = view?.getZoom() ?? 0;
  view?.animate({ zoom: zoom + 1, duration: 200 });
}

This keeps the map responsive without forcing React to re-render the whole map.

Suggested File Structure

For a real project, I prefer separating map setup from data and UI.

src/
  components/
    map/
      OpenLayersMap.tsx
      mapStyles.ts
      markerLayer.ts
      useMapInteractions.ts
  data/
    locations.ts

This makes the map easier to test and easier to extend when the project grows.

Best Practices

Use useRef for the map instance. The map is not normal React state and should not trigger re-renders.

Clean up with setTarget(undefined). This prevents memory leaks when moving between pages.

Normalize coordinates early. Keep longitude and latitude clear in your data, then convert them at the map boundary with fromLonLat.

Separate layers by responsibility. Base map, markers, drawings, selected features, and overlays should not all live in one layer.

Avoid putting too much logic in one component. A map can become complex quickly, so split styling, layers, interactions, and data loading into small modules.

Conclusion

React and OpenLayers work very well together when each tool has a clear job. React should manage the page, controls, and data flow. OpenLayers should manage the map rendering, layers, and geospatial interactions.

That structure gives you a map that is smooth for users and maintainable for developers. It is the same mindset I use when building front-end experiences: start with a simple working foundation, then keep every new feature organized enough to grow.

Stay up-to-date with my words