Ctrl + k

or run

tessl search
Log in

Version

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
npmpkg:npm/react-leaflet@5.0.x

docs

examples

edge-cases.mdreal-world-scenarios.md
index.md
tile.json

tessl/npm-react-leaflet

tessl install tessl/npm-react-leaflet@5.0.3

React components for Leaflet maps

edge-cases.mddocs/examples/

Edge Cases & Advanced Scenarios

Solutions for complex scenarios, edge cases, and advanced React Leaflet usage.

Table of Contents

  • Performance Optimization
  • Server-Side Rendering
  • Memory Management
  • Accessibility
  • Mobile & Touch
  • Testing
  • Error Handling
  • Advanced Patterns

Performance Optimization

Large Marker Sets (1000+ markers)

import { MapContainer, TileLayer } from "react-leaflet";
import { useMemo, useState, useEffect } from "react";
import { useMap } from "react-leaflet";

// Use Canvas renderer for better performance
<MapContainer preferCanvas={true} center={[51.505, -0.09]} zoom={13}>
  <TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
  <VirtualizedMarkers markers={largeMarkerArray} />
</MapContainer>

// Only render visible markers
function VirtualizedMarkers({ markers }) {
  const map = useMap();
  const [visibleMarkers, setVisibleMarkers] = useState([]);

  useEffect(() => {
    const updateVisibleMarkers = () => {
      const bounds = map.getBounds();
      const visible = markers.filter(marker => 
        bounds.contains(marker.position)
      );
      setVisibleMarkers(visible.slice(0, 100)); // Limit to 100
    };

    updateVisibleMarkers();
    map.on('moveend', updateVisibleMarkers);
    map.on('zoomend', updateVisibleMarkers);

    return () => {
      map.off('moveend', updateVisibleMarkers);
      map.off('zoomend', updateVisibleMarkers);
    };
  }, [map, markers]);

  return (
    <>
      {visibleMarkers.map(marker => (
        <Marker key={marker.id} position={marker.position} />
      ))}
    </>
  );
}

Debounced Map Events

import { useMapEvents } from "react-leaflet";
import { useCallback, useRef } from "react";

function DebouncedMapEvents({ onMoveEnd, delay = 300 }) {
  const timeoutRef = useRef<NodeJS.Timeout>();

  const debouncedMoveEnd = useCallback(() => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }
    timeoutRef.current = setTimeout(() => {
      onMoveEnd();
    }, delay);
  }, [onMoveEnd, delay]);

  useMapEvents({
    moveend: debouncedMoveEnd,
  });

  return null;
}

Memoized Components

import { memo } from "react";

const MemoizedMarker = memo(({ position, title }) => (
  <Marker position={position}>
    <Popup>{title}</Popup>
  </Marker>
), (prevProps, nextProps) => {
  return (
    prevProps.position[0] === nextProps.position[0] &&
    prevProps.position[1] === nextProps.position[1] &&
    prevProps.title === nextProps.title
  );
});

Server-Side Rendering

Next.js App Router

// app/map/page.tsx
import dynamic from 'next/dynamic';

const Map = dynamic(() => import('@/components/Map'), {
  ssr: false,
  loading: () => (
    <div style={{ height: '400px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
      Loading map...
    </div>
  ),
});

export default function MapPage() {
  return (
    <div>
      <h1>Map Page</h1>
      <Map />
    </div>
  );
}

Next.js Pages Router

// pages/map.tsx
import dynamic from 'next/dynamic';

const MapWithNoSSR = dynamic(() => import('../components/Map'), {
  ssr: false,
});

export default function MapPage() {
  return <MapWithNoSSR />;
}

Gatsby

// gatsby-browser.js
export const onClientEntry = () => {
  // Ensure Leaflet is only loaded client-side
  if (typeof window !== 'undefined') {
    require('leaflet/dist/leaflet.css');
  }
};

// Component
import React, { useEffect, useState } from 'react';

function Map() {
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    setIsClient(true);
  }, []);

  if (!isClient) {
    return <div>Loading map...</div>;
  }

  const { MapContainer, TileLayer } = require('react-leaflet');
  
  return (
    <MapContainer center={[51.505, -0.09]} zoom={13}>
      <TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
    </MapContainer>
  );
}

Memory Management

Cleanup Event Listeners

function MapComponent() {
  const map = useMap();

  useEffect(() => {
    const handleClick = (e) => {
      console.log('Map clicked', e.latlng);
    };

    map.on('click', handleClick);

    // CRITICAL: Clean up event listener
    return () => {
      map.off('click', handleClick);
    };
  }, [map]);

  return null;
}

Cleanup Intervals and Timeouts

function RealTimeUpdates() {
  const [data, setData] = useState([]);

  useEffect(() => {
    const interval = setInterval(() => {
      // Fetch updates
      fetchData().then(setData);
    }, 5000);

    // CRITICAL: Clear interval on unmount
    return () => {
      clearInterval(interval);
    };
  }, []);

  return <>{/* Render data */}</>;
}

Remove Layers on Unmount

function CustomLayer() {
  const map = useMap();
  const layerRef = useRef(null);

  useEffect(() => {
    const layer = L.circle([51.505, -0.09], { radius: 200 });
    layer.addTo(map);
    layerRef.current = layer;

    // CRITICAL: Remove layer on unmount
    return () => {
      if (layerRef.current) {
        map.removeLayer(layerRef.current);
      }
    };
  }, [map]);

  return null;
}

Accessibility

Keyboard Navigation

<MapContainer
  center={[51.505, -0.09]}
  zoom={13}
  keyboard={true}
  keyboardPanDelta={80}
  tabIndex={0}
  aria-label="Interactive map"
  style={{ height: '500px' }}
>
  <TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</MapContainer>

Screen Reader Support

function AccessibleMap() {
  const [announcements, setAnnouncements] = useState('');

  return (
    <div>
      <div 
        role="status" 
        aria-live="polite" 
        aria-atomic="true"
        className="sr-only"
      >
        {announcements}
      </div>
      
      <MapContainer 
        center={[51.505, -0.09]} 
        zoom={13}
        aria-label="Interactive map showing locations"
      >
        <TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
        <MapEvents setAnnouncements={setAnnouncements} />
      </MapContainer>
    </div>
  );
}

function MapEvents({ setAnnouncements }) {
  useMapEvents({
    zoomend: (e) => {
      setAnnouncements(`Zoom level changed to ${e.target.getZoom()}`);
    },
    moveend: (e) => {
      const center = e.target.getCenter();
      setAnnouncements(`Map centered at ${center.lat.toFixed(2)}, ${center.lng.toFixed(2)}`);
    },
  });
  return null;
}

ARIA Labels for Markers

<Marker 
  position={[51.505, -0.09]}
  title="City Hall"
  alt="City Hall marker"
>
  <Popup>
    <div role="dialog" aria-label="City Hall information">
      <h3>City Hall</h3>
      <p>123 Main Street</p>
    </div>
  </Popup>
</Marker>

Mobile & Touch

Touch-Optimized Controls

<MapContainer
  center={[51.505, -0.09]}
  zoom={13}
  touchZoom={true}
  tap={true}
  tapTolerance={15}
  dragging={true}
  scrollWheelZoom={false}
  doubleClickZoom={true}
>
  <TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</MapContainer>

Responsive Map Height

import { useState, useEffect } from 'react';

function ResponsiveMap() {
  const [mapHeight, setMapHeight] = useState('400px');

  useEffect(() => {
    const updateHeight = () => {
      if (window.innerWidth < 768) {
        setMapHeight('300px');
      } else if (window.innerWidth < 1024) {
        setMapHeight('500px');
      } else {
        setMapHeight('600px');
      }
    };

    updateHeight();
    window.addEventListener('resize', updateHeight);
    return () => window.removeEventListener('resize', updateHeight);
  }, []);

  return (
    <MapContainer 
      center={[51.505, -0.09]} 
      zoom={13}
      style={{ height: mapHeight, width: '100%' }}
    >
      <TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
    </MapContainer>
  );
}

Disable Scroll Zoom Until Focused

function ScrollControlledMap() {
  const map = useMap();

  useEffect(() => {
    map.scrollWheelZoom.disable();

    const enableZoom = () => map.scrollWheelZoom.enable();
    const disableZoom = () => map.scrollWheelZoom.disable();

    map.on('focus', enableZoom);
    map.on('blur', disableZoom);

    return () => {
      map.off('focus', enableZoom);
      map.off('blur', disableZoom);
    };
  }, [map]);

  return null;
}

Testing

Unit Testing with Jest

import { render } from '@testing-library/react';
import { MapContainer, Marker } from 'react-leaflet';

// Mock Leaflet
jest.mock('leaflet', () => ({
  map: jest.fn(() => ({
    on: jest.fn(),
    off: jest.fn(),
    remove: jest.fn(),
    setView: jest.fn(),
    getZoom: jest.fn(() => 13),
  })),
  tileLayer: jest.fn(() => ({
    addTo: jest.fn(),
  })),
  marker: jest.fn(() => ({
    addTo: jest.fn(),
    remove: jest.fn(),
    bindPopup: jest.fn(),
  })),
}));

test('renders map container', () => {
  const { container } = render(
    <MapContainer center={[51.505, -0.09]} zoom={13}>
      <Marker position={[51.505, -0.09]} />
    </MapContainer>
  );
  
  expect(container).toBeInTheDocument();
});

Integration Testing

import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('map responds to marker click', async () => {
  const handleClick = jest.fn();
  
  const { getByRole } = render(
    <MapContainer center={[51.505, -0.09]} zoom={13}>
      <Marker 
        position={[51.505, -0.09]}
        eventHandlers={{ click: handleClick }}
      />
    </MapContainer>
  );
  
  // Simulate interaction
  await waitFor(() => {
    // Add assertions
  });
});

Error Handling

Error Boundary

import { Component, ReactNode } from 'react';

class MapErrorBoundary extends Component<
  { children: ReactNode },
  { hasError: boolean; error: Error | null }
> {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error('Map error:', error, errorInfo);
    // Log to error reporting service
  }

  render() {
    if (this.state.hasError) {
      return (
        <div style={{ padding: '20px', textAlign: 'center' }}>
          <h2>Map failed to load</h2>
          <p>{this.state.error?.message}</p>
          <button onClick={() => window.location.reload()}>
            Reload Page
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

// Usage
<MapErrorBoundary>
  <MapContainer center={[51.505, -0.09]} zoom={13}>
    <TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
  </MapContainer>
</MapErrorBoundary>

Tile Load Error Handling

function RobustTileLayer({ url, fallbackUrl }) {
  const [currentUrl, setCurrentUrl] = useState(url);
  const [errorCount, setErrorCount] = useState(0);

  const handleTileError = useCallback(() => {
    setErrorCount(prev => prev + 1);
    
    if (errorCount > 10 && fallbackUrl) {
      console.warn('Switching to fallback tile server');
      setCurrentUrl(fallbackUrl);
    }
  }, [errorCount, fallbackUrl]);

  return (
    <TileLayer
      url={currentUrl}
      eventHandlers={{
        tileerror: handleTileError,
      }}
    />
  );
}

Advanced Patterns

Custom Hook for Map State

function useMapState() {
  const map = useMap();
  const [state, setState] = useState({
    center: map.getCenter(),
    zoom: map.getZoom(),
    bounds: map.getBounds(),
  });

  useMapEvents({
    moveend: () => {
      setState({
        center: map.getCenter(),
        zoom: map.getZoom(),
        bounds: map.getBounds(),
      });
    },
  });

  return state;
}

// Usage
function MapInfo() {
  const { center, zoom } = useMapState();
  return (
    <div>
      Center: {center.lat.toFixed(4)}, {center.lng.toFixed(4)} | Zoom: {zoom}
    </div>
  );
}

Geolocation Hook

function useGeolocation() {
  const map = useMap();
  const [position, setPosition] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    map.locate({ setView: true, maxZoom: 16 });

    const onLocationFound = (e) => {
      setPosition(e.latlng);
      setError(null);
    };

    const onLocationError = (e) => {
      setError(e.message);
    };

    map.on('locationfound', onLocationFound);
    map.on('locationerror', onLocationError);

    return () => {
      map.off('locationfound', onLocationFound);
      map.off('locationerror', onLocationError);
    };
  }, [map]);

  return { position, error };
}

Context-Based State Management

import { createContext, useContext, useState } from 'react';

const MapStateContext = createContext(null);

export function MapStateProvider({ children }) {
  const [selectedFeature, setSelectedFeature] = useState(null);
  const [filters, setFilters] = useState({});
  const [viewState, setViewState] = useState({
    center: [51.505, -0.09],
    zoom: 13,
  });

  return (
    <MapStateContext.Provider 
      value={{ 
        selectedFeature, 
        setSelectedFeature,
        filters,
        setFilters,
        viewState,
        setViewState
      }}
    >
      {children}
    </MapStateContext.Provider>
  );
}

export function useMapState() {
  const context = useContext(MapStateContext);
  if (!context) {
    throw new Error('useMapState must be used within MapStateProvider');
  }
  return context;
}

Coordinate Order Conversion

// Leaflet uses [lat, lng]
const leafletCoords: LatLngExpression = [51.505, -0.09];

// GeoJSON uses [lng, lat]
const geoJSONCoords = [-0.09, 51.505];

// Conversion utilities
function geoJSONToLeaflet(coords: [number, number]): [number, number] {
  return [coords[1], coords[0]];
}

function leafletToGeoJSON(coords: [number, number]): [number, number] {
  return [coords[1], coords[0]];
}

React Concurrent Mode

import { useDeferredValue, useTransition } from 'react';

function ConcurrentMarkers({ markers }) {
  const [isPending, startTransition] = useTransition();
  const deferredMarkers = useDeferredValue(markers);

  return (
    <>
      {isPending && <div>Updating markers...</div>}
      {deferredMarkers.map(marker => (
        <Marker key={marker.id} position={marker.position} />
      ))}
    </>
  );
}

Additional Resources

  • Real-World Scenarios
  • API Reference
  • Quick Start Guide