Plotting Fire Incident Data With Leaflet And React

Leaflet remains one of the most popular open-source JavaScript libraries for building interactive, mobile-friendly maps. It weighs roughly 38KB, works across modern and legacy browsers, and can be extended through a large plugin ecosystem. Major newsrooms such as NPR, the Washington Post, and the Boston Globe rely on it for data-driven visual stories — for instance, the San Francisco Chronicle used it for its California fire tracker, showing both origin points and fire trajectories.

In this tutorial, we will build a React application that plots non-medical fire incidents reported to the San Francisco Fire Department. The final result will be a map with markers that reveal incident details in popups. We will need three tools: Leaflet for the map rendering, React for the UI, and React-Leaflet, which provides React components that wrap the core Leaflet API.

Leaflet Basics With Vanilla JavaScript

Before introducing React, it helps to see how Leaflet works on its own. Start by creating an index.html file and linking Leaflet’s CSS and JavaScript files in the document head — the CSS must come first. You will also need a container element, typically a <div>, to hold the map.

<div id="mapid"></div>

The container needs an explicit height or the map will not render. Add the following to your stylesheet:

#mapid { height: 1000px; }

When you initialize the map, the DOM element must already exist. Calling L.map('mapid') before the div is available will throw an error.

Uncaught Error: Map container not found

Initializing The Map

The Leaflet Map class takes two arguments: a string representing the DOM ID and an optional options object. The two most important options are center, which sets the initial geographic coordinates, and zoom, which sets the initial zoom level. Both default to undefined. For a city-level view, zoom level 13 is typically sufficient.

const myMap = L.map('mapid', {
 center: [37.7749, -122.4194],
  zoom: 13
})

Alternatively, the setView() method accomplishes the same initialization by accepting an array of coordinates and an integer zoom value.

const myMap = L.map('map').setView([37.7749, -122.4194], 13);

Mouse and touch interactions are enabled by default, along with zoom and attribution controls.

Adding Tile Layers

To display geographic context, you must add a tile layer. The TileLayer class requires a URL template from the tile provider, attribution text, and a maximum zoom level. In this example we use Mapbox’s static tiles API, which requires an access token from your Mapbox account.

L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', { 
attribution: 'Map data © <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery (c) <a href="https://www.mapbox.com/">Mapbox</a>',
maxZoom: 18, 
id: 'mapbox/streets-v11', 
accessToken: 'your.mapbox.access.token' }).addTo(mymap);

Markers And Popups

Markers point to specific locations. Instantiate the Marker class with coordinates and add it to the map. For example, the following markers represent Twin Peaks and the Point Bonita Lighthouse:

const marker = L.marker([37.7544, -122.4477]).addTo(mymap);
const circle = L.circle([37.8157, -122.5295], {
 color: 'gold',
 fillColor: '#f03',
 fillOpacity: 0.5,
 radius: 200
}).addTo(mymap);

Circles work similarly via the Circle class, accepting options like radius and color.

To provide additional context, the bindPopup method attaches an HTML string to a marker or circle, causing the content to appear when the user clicks the element.

circle.bindPopup("I am pointing to Point Bonita Lighthouse");

marker.bindPopup("I am pointing to Twin Peaks");

Building The React Application

Now we will replicate and extend the same functionality using React-Leaflet. First, gather an API key from the San Francisco Open Data portal. After creating an account, go to the “manage” section, click “Create New API Key,” and copy both the key ID and key secret. These will authenticate your requests to the data portal.

Create a new React application and install the required packages:

npx create-react-app react-fire-incidents
cd react-fire-incidents
npm install react-leaflet leaflet

Structuring The Application

Create a /components folder inside the src directory and place a new file called Map.js there. Update App.js to import react-leaflet, axios, and the new Map component.

import React, { Component, Fragment } from 'react';
import axios from 'axios';
import Map from './components/Map'

In the App class, maintain an incidents array in state. On component mount, issue a GET request to the data portal endpoint — search for “fire incidents” under the “Browse Data” section and copy the API URL from the button in the top-right corner. Pass the endpoint, a limit of 500 records, and the app token as request parameters. The response updates the incidents array.

class App extends Component {
 state = {
   incidents: [],
 }
 render() {
   return (
     <div> </div>
   );
 }
}
export default App;
async componentDidMount() {
   const res = await axios.get('https://data.sfgov.org/resource/wr8u-xric.json', {
     params: {
       "$limit": 500,
       "$$app_token": YOUR_APP_TOKEN
     }
   })
   const incidents = res.data;
   this.setState({incidents: incidents });
 };

The complete App.js should look like this:

class App extends Component {
state = {
  incidents: [],
}

async componentDidMount() {
 const res = await axios.get('https://data.sfgov.org/resource/wr8u-xric.json', {
   params: {
     "$limit": 500,
     "$$app_token": YOUR_APP_TOKEN
   }
 })
 const incidents = res.data;
 this.setState({incidents: incidents });
};
render() {
 return (
<Map incidents={this.state.incidents}/>
 );
}
}
export default App;

The Map Component

Inside Map.js, import the Map, TileLayer, Marker, and Popup components from react-leaflet.

import React, { Component } from 'react'
import { Map, TileLayer, Marker, Popup } from 'react-leaflet'

Store the initial coordinates and zoom level in the component state with lat, lng, and zoom variables.

export default class Map extends Component {
   state = {
       lat: 37.7749,
       lng: -122.4194,
       zoom: 13,
   }
   render() {
       return (
     <div></div>
        )
    }
}

If the incidents array is empty, render a message such as “Data is Loading.” Otherwise, render the map, passing the center coordinates, zoom level, and styling to the Map component. The TileLayer receives the same attribution and URL as in the vanilla example.

render() {
       return (
          this.props.incidents ?
              <Map 
                 center={[this.state.lat, this.state.lng]} 
                 zoom={this.state.zoom} 
                 style={{ width: '100%', height: '900px'}}
              >
              <TileLayer
                attribution='&copy <a href="https://osm.org/copyright">OpenStreetMap</a> contributors'
                url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
               />
             </Map>
               :
               'Data is loading...'
       )
   }
}

Loop through props.incident and render a Marker for each entry. React requires a unique key prop on every item in the loop. Inside each marker, provide a Popup component with relevant incident details.

<Map 
    center={[this.state.lat, this.state.lng]} 
    zoom={this.state.zoom} 
    style={{ width: '100%', height: '900px'}}>
       <TileLayer
          attribution='&copy <a href="https://osm.org/copyright">OpenStreetMap</a> contributors'
          url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        />
        {
          this.props.incidents.map(incident => {
               const point = [incident['point']['coordinates'][1],                 incident['point']['coordinates'][0]]
         

return (
    <Marker position={point} key={incident['incident_number']} >
         <Popup>
            <span>ADDRESS: {incident['address']}, {incident['city']} - {incident['zip_code']}</span>
          <br/>
            <span>BATTALION: {incident['battalion']}</span><br/>
         </Popup>
     </Marker>
  )
 })
}
</Map>

With the code in place, running the app displays a map of San Francisco with 500 markers representing fire incident locations. Clicking any marker opens a popup with the corresponding description from the dataset.

Going Beyond The Basics

This example only scratches the surface of what Leaflet can do. To expand your map, consider adding multiple layer controls, custom marker icons, or a choropleth visualization that colors regions based on data values. The Leaflet documentation offers dedicated examples for each of these scenarios.