tessl install tessl/npm-react-leaflet@5.0.3React components for Leaflet maps
The MapContainer component is the root component that creates and manages the Leaflet Map instance. All other react-leaflet components must be children (direct or nested) of MapContainer to access the map context.
Creates a Leaflet map instance and provides it to child components via React context.
/**
* Root component that creates a Leaflet map instance
* @param props - MapContainer properties
* @param ref - Optional ref to access the Leaflet Map instance
*/
const MapContainer: ForwardRefExoticComponent<MapContainerProps & RefAttributes<MapRef>>;
interface MapContainerProps extends MapOptions {
/** Initial map center (latitude, longitude) */
center?: LatLngExpression;
/** Initial zoom level */
zoom?: number;
/** Fit map to these bounds instead of using center/zoom */
bounds?: LatLngBoundsExpression;
/** Options for fitting bounds */
boundsOptions?: FitBoundsOptions;
/** Child components (layers, markers, controls, etc.) */
children?: ReactNode;
/** CSS class for the map container div */
className?: string;
/** HTML id for the map container div */
id?: string;
/** Content displayed before map initializes */
placeholder?: ReactNode;
/** Inline styles for the map container div */
style?: CSSProperties;
/** Callback invoked when map is ready */
whenReady?: () => void;
// Inherited from Leaflet MapOptions:
/** Prefer Canvas over SVG for vector layers */
preferCanvas?: boolean;
/** Minimum zoom level */
minZoom?: number;
/** Maximum zoom level */
maxZoom?: number;
/** Maximum bounds that user can navigate to */
maxBounds?: LatLngBoundsExpression;
/** Coordinate reference system */
crs?: CRS;
/** Whether map is draggable with mouse/touch */
dragging?: boolean;
/** Whether touch gestures rotate the map */
touchZoom?: boolean;
/** Whether scroll wheel zooms the map */
scrollWheelZoom?: boolean;
/** Whether double click zooms the map */
doubleClickZoom?: boolean;
/** Whether box zoom (shift+drag) is enabled */
boxZoom?: boolean;
/** Whether map automatically handles browser window resize */
trackResize?: boolean;
/** Whether map has zoom control */
zoomControl?: boolean;
/** Whether attribution control is shown */
attributionControl?: boolean;
/** And many more Leaflet MapOptions... */
}
type MapRef = Map | null;Usage Example:
import { MapContainer, TileLayer } from "react-leaflet";
import { useRef } from "react";
function App() {
const mapRef = useRef<Map>(null);
return (
<MapContainer
center={[51.505, -0.09]}
zoom={13}
style={{ height: "100vh", width: "100%" }}
scrollWheelZoom={false}
ref={mapRef}
whenReady={() => {
console.log("Map is ready");
}}
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
/>
</MapContainer>
);
}MapContainer supports two initialization modes:
1. Center and Zoom:
<MapContainer center={[51.505, -0.09]} zoom={13}>2. Bounds:
<MapContainer
bounds={[[51.49, -0.12], [51.52, -0.06]]}
boundsOptions={{ padding: [50, 50] }}
>3. Neither (map starts at default view):
<MapContainer zoom={2}>
{/* Use hooks or refs to set initial view programmatically */}
</MapContainer>Using Ref:
import { useRef, useEffect } from "react";
import { MapContainer } from "react-leaflet";
import type { Map } from "leaflet";
function MyComponent() {
const mapRef = useRef<Map>(null);
useEffect(() => {
if (mapRef.current) {
// Access Leaflet Map instance
const map = mapRef.current;
map.setView([51.505, -0.09], 13);
}
}, []);
return <MapContainer ref={mapRef} center={[51.505, -0.09]} zoom={13}>
{/* ... */}
</MapContainer>;
}Using Hook (in child component):
import { useMap } from "react-leaflet";
function MyControl() {
const map = useMap();
const handleClick = () => {
map.setView([51.505, -0.09], 13);
};
return <button onClick={handleClick}>Reset View</button>;
}Using whenReady Callback:
function App() {
const handleMapReady = useCallback(() => {
console.log("Map initialized and ready");
// Access map via ref if needed
}, []);
return (
<MapContainer
center={[51.505, -0.09]}
zoom={13}
whenReady={handleMapReady}
>
{/* ... */}
</MapContainer>
);
}Non-Reactive Props: Most MapContainer props are only used during initialization. To update the map dynamically, use refs or hooks to access the Leaflet Map instance and call Leaflet methods directly.
Required Style: The container must have explicit height and width:
style={{ height: "400px", width: "100%" }}One Map Per Container: Each MapContainer creates exactly one Leaflet Map instance. To display multiple maps, use multiple MapContainer components.
Context Provider: MapContainer provides LeafletContext to all children, enabling them to access the map instance.
Placeholder: The placeholder prop displays content before the map initializes, useful for loading states:
<MapContainer placeholder={<div>Loading map...</div>} {/* ... */}>WhenReady Callback: Use whenReady for code that should run after map initialization:
<MapContainer whenReady={() => console.log("Map ready")} {/* ... */}>Unmounting: When MapContainer unmounts, it automatically cleans up the Leaflet map instance and removes all event listeners.
Multiple Instances: You can render multiple MapContainer components on the same page, each with its own independent map instance.
Conditional Rendering: If you conditionally render MapContainer, the map will be recreated each time it mounts:
{showMap && <MapContainer center={[51.505, -0.09]} zoom={13}>...</MapContainer>}Ref Stability: The ref is populated after the component mounts. Always check if ref.current is not null before using it.
type MapRef = Map | null;
// Usage with ref
import type { Map } from 'leaflet';
const mapRef = useRef<Map | null>(null);Ref type for accessing the Leaflet Map instance. Will be null before the map initializes.
// From Leaflet
type LatLngExpression =
| LatLng
| [number, number]
| [number, number, number]
| { lat: number; lng: number }
| { lat: number; lng: number; alt?: number };Flexible type for specifying latitude/longitude positions.
// From Leaflet
type LatLngBoundsExpression =
| LatLngBounds
| [LatLngExpression, LatLngExpression];Type for specifying rectangular geographic bounds.
// From Leaflet (partial - see Leaflet docs for complete list)
interface MapOptions {
preferCanvas?: boolean;
attributionControl?: boolean;
zoomControl?: boolean;
closePopupOnClick?: boolean;
boxZoom?: boolean;
doubleClickZoom?: boolean | 'center';
dragging?: boolean;
crs?: CRS;
center?: LatLngExpression;
zoom?: number;
minZoom?: number;
maxZoom?: number;
layers?: Layer[];
maxBounds?: LatLngBoundsExpression;
renderer?: Renderer;
zoomAnimation?: boolean;
zoomAnimationThreshold?: number;
fadeAnimation?: boolean;
markerZoomAnimation?: boolean;
transform3DLimit?: number;
inertia?: boolean;
inertiaDeceleration?: number;
inertiaMaxSpeed?: number;
easeLinearity?: number;
worldCopyJump?: boolean;
maxBoundsViscosity?: number;
keyboard?: boolean;
keyboardPanDelta?: number;
scrollWheelZoom?: boolean | 'center';
wheelDebounceTime?: number;
wheelPxPerZoomLevel?: number;
tapHold?: boolean;
tapTolerance?: number;
touchZoom?: boolean | 'center';
bounceAtZoomLimits?: boolean;
}Complete MapOptions type from Leaflet with all map configuration options.
// From Leaflet
interface FitBoundsOptions extends ZoomPanOptions {
/** Padding around bounds in pixels */
paddingTopLeft?: Point;
/** Padding around bounds in pixels */
paddingBottomRight?: Point;
/** Equivalent to setting both paddingTopLeft and paddingBottomRight */
padding?: Point;
/** Maximum zoom level to use */
maxZoom?: number;
}Options for fitting map to bounds.
Since MapContainer props are immutable, use a child component with hooks to update the view:
function ChangeView({ center, zoom }: { center: LatLngExpression; zoom: number }) {
const map = useMap();
useEffect(() => {
map.setView(center, zoom);
}, [map, center, zoom]);
return null;
}
function App() {
const [center, setCenter] = useState<LatLngExpression>([51.505, -0.09]);
const [zoom, setZoom] = useState(13);
return (
<MapContainer center={center} zoom={zoom}>
<ChangeView center={center} zoom={zoom} />
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</MapContainer>
);
}function FlyToLocation({ center, zoom }: { center: LatLngExpression; zoom: number }) {
const map = useMap();
useEffect(() => {
map.flyTo(center, zoom, {
duration: 2, // Animation duration in seconds
});
}, [map, center, zoom]);
return null;
}function FitBounds({ bounds }: { bounds: LatLngBoundsExpression }) {
const map = useMap();
useEffect(() => {
map.fitBounds(bounds, {
padding: [50, 50],
maxZoom: 15,
});
}, [map, bounds]);
return null;
}function MapEvents() {
const map = useMapEvents({
zoomend: () => {
console.log("Zoom level:", map.getZoom());
},
moveend: () => {
const center = map.getCenter();
console.log("Center:", center.lat, center.lng);
},
});
return null;
}
function App() {
return (
<MapContainer center={[51.505, -0.09]} zoom={13}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<MapEvents />
</MapContainer>
);
}<MapContainer
center={[51.505, -0.09]}
zoom={13}
maxBounds={[[51.4, -0.2], [51.6, 0.0]]}
maxBoundsViscosity={1.0} // Prevents dragging outside bounds
minZoom={10}
maxZoom={18}
>
{/* ... */}
</MapContainer>import L from "leaflet";
<MapContainer
center={[0, 0]}
zoom={0}
crs={L.CRS.Simple} // For non-geographic maps (e.g., game maps, floor plans)
>
{/* ... */}
</MapContainer><MapContainer
center={[51.505, -0.09]}
zoom={13}
preferCanvas={true} // Use Canvas renderer for better performance with many vectors
zoomAnimation={false} // Disable zoom animation for better performance
fadeAnimation={false} // Disable fade animation
markerZoomAnimation={false} // Disable marker zoom animation
>
{/* ... */}
</MapContainer><MapContainer
center={[51.505, -0.09]}
zoom={13}
dragging={false}
touchZoom={false}
doubleClickZoom={false}
scrollWheelZoom={false}
boxZoom={false}
keyboard={false}
zoomControl={false}
>
{/* Static map display */}
</MapContainer>Problem: Map container is empty or shows gray area.
Solutions:
Ensure Leaflet CSS is imported:
import "leaflet/dist/leaflet.css";Set explicit height on container:
<MapContainer style={{ height: "400px", width: "100%" }}>Verify center and zoom are provided:
<MapContainer center={[51.505, -0.09]} zoom={13}>Problem: Map doesn't resize properly when container size changes.
Solution: Call invalidateSize() after resize:
function ResizableMap() {
const mapRef = useRef<Map>(null);
useEffect(() => {
const handleResize = () => {
if (mapRef.current) {
mapRef.current.invalidateSize();
}
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return <MapContainer ref={mapRef} center={[51.505, -0.09]} zoom={13}>
{/* ... */}
</MapContainer>;
}Problem: Leaflet doesn't work with SSR (Next.js, Gatsby, etc.).
Solution: Dynamically import MapContainer client-side only:
Next.js:
import dynamic from 'next/dynamic';
const MapContainer = dynamic(
() => import('react-leaflet').then((mod) => mod.MapContainer),
{ ssr: false }
);React (with lazy loading):
const MapComponent = lazy(() => import('./MapComponent'));
function App() {
return (
<Suspense fallback={<div>Loading map...</div>}>
<MapComponent />
</Suspense>
);
}function MultiMapPage() {
return (
<div>
<MapContainer center={[51.505, -0.09]} zoom={13} style={{ height: "400px" }}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</MapContainer>
<MapContainer center={[48.8566, 2.3522]} zoom={13} style={{ height: "400px" }}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</MapContainer>
</div>
);
}Problem: Map recreates when conditionally rendered.
Solution: Use CSS to show/hide instead:
function ConditionalMap({ show }: { show: boolean }) {
return (
<div style={{ display: show ? 'block' : 'none' }}>
<MapContainer center={[51.505, -0.09]} zoom={13} style={{ height: "400px" }}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</MapContainer>
</div>
);
}Problem: Scroll wheel zooms map when user tries to scroll page.
Solutions:
<MapContainer scrollWheelZoom={false}>function ScrollWheelZoomControl() {
const map = useMap();
useEffect(() => {
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;
}Problem: Memory leaks when map is unmounted and remounted frequently.
Solution: Ensure proper cleanup in child components:
function MapComponent() {
const map = useMap();
useEffect(() => {
const handler = () => console.log('Map moved');
map.on('moveend', handler);
// IMPORTANT: Clean up event listeners
return () => {
map.off('moveend', handler);
};
}, [map]);
return null;
}Problem: Map doesn't fit bounds as expected.
Solution: Use appropriate padding and options:
<MapContainer
bounds={[[51.49, -0.12], [51.52, -0.06]]}
boundsOptions={{
padding: [50, 50], // Add padding around bounds
maxZoom: 15, // Prevent zooming in too much
}}
>Problem: Touch gestures conflict with page scrolling.
Solution: Configure touch interaction:
<MapContainer
touchZoom={true}
tap={true}
tapTolerance={15} // Increase tap tolerance for better mobile UX
dragging={true}
scrollWheelZoom={false} // Disable on mobile
>Always Set Height: MapContainer requires explicit height to display properly.
Use Refs for Imperative Operations: Access the map instance via refs for operations like setView, fitBounds, etc.
Memoize Callbacks: Use useCallback for whenReady and other callbacks to prevent unnecessary re-renders.
Lazy Load for SSR: Always use dynamic imports for server-side rendered applications.
Clean Up Event Listeners: Remove event listeners in cleanup functions to prevent memory leaks.
Optimize for Performance: Use preferCanvas for maps with many vector shapes.
Handle Resize Events: Call invalidateSize() when container size changes.
Test on Mobile: Ensure touch gestures work correctly on mobile devices.
Provide Loading States: Use placeholder prop for better UX during map initialization.
Document Map Configuration: Document why specific MapOptions are used for future maintainability.
import { render, screen } from '@testing-library/react';
import { MapContainer, TileLayer } from 'react-leaflet';
test('renders map container', () => {
const { container } = render(
<MapContainer center={[51.505, -0.09]} zoom={13} style={{ height: '400px' }}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</MapContainer>
);
expect(container.querySelector('.leaflet-container')).toBeInTheDocument();
});test('shows placeholder before map loads', () => {
render(
<MapContainer
center={[51.505, -0.09]}
zoom={13}
placeholder={<div>Loading map...</div>}
>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</MapContainer>
);
// Assert placeholder is shown
});test('calls whenReady callback', () => {
const handleReady = jest.fn();
render(
<MapContainer
center={[51.505, -0.09]}
zoom={13}
whenReady={handleReady}
>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</MapContainer>
);
// Assert callback was called
});<MapContainer
center={[51.505, -0.09]}
zoom={13}
keyboard={true}
keyboardPanDelta={80}
aria-label="Interactive map showing locations"
tabIndex={0}
style={{ height: '500px' }}
>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</MapContainer><div role="application" aria-label="Map application">
<MapContainer
center={[51.505, -0.09]}
zoom={13}
style={{ height: '500px' }}
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='© OpenStreetMap contributors'
/>
<div role="status" aria-live="polite" className="sr-only">
Map loaded. Use arrow keys to pan, plus and minus keys to zoom.
</div>
</MapContainer>
</div>import { MapContainer, TileLayer } from 'react-leaflet';
import type { Map, MapOptions, LatLngExpression } from 'leaflet';
import { useRef, useEffect, CSSProperties } from 'react';
interface MapComponentProps {
center: LatLngExpression;
zoom: number;
style?: CSSProperties;
options?: Partial<MapOptions>;
}
export function MapComponent({ center, zoom, style, options }: MapComponentProps) {
const mapRef = useRef<Map | null>(null);
useEffect(() => {
if (mapRef.current) {
// Type-safe access to map instance
const map: Map = mapRef.current;
console.log('Current zoom:', map.getZoom());
}
}, []);
return (
<MapContainer
ref={mapRef}
center={center}
zoom={zoom}
style={style}
{...options}
>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</MapContainer>
);
}
## Examples
### Full-Featured Map Setup
```typescript
function FullFeaturedMap() {
const mapRef = useRef<Map>(null);
const [isReady, setIsReady] = useState(false);
const handleMapReady = useCallback(() => {
setIsReady(true);
console.log("Map initialized");
}, []);
useEffect(() => {
if (isReady && mapRef.current) {
// Perform operations after map is ready
mapRef.current.on('zoomend', () => {
console.log('Zoom changed:', mapRef.current?.getZoom());
});
}
}, [isReady]);
return (
<MapContainer
ref={mapRef}
center={[51.505, -0.09]}
zoom={13}
style={{ height: "100vh", width: "100%" }}
scrollWheelZoom={false}
zoomControl={false}
attributionControl={false}
maxBounds={[[51.4, -0.2], [51.6, 0.0]]}
minZoom={10}
maxZoom={18}
whenReady={handleMapReady}
placeholder={<div>Loading map...</div>}
>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
{/* Additional layers and controls */}
</MapContainer>
);
}function ResponsiveMap() {
const containerRef = useRef<HTMLDivElement>(null);
const mapRef = useRef<Map>(null);
useEffect(() => {
const resizeObserver = new ResizeObserver(() => {
if (mapRef.current) {
mapRef.current.invalidateSize();
}
});
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
}
return () => {
resizeObserver.disconnect();
};
}, []);
return (
<div ref={containerRef} style={{ width: '100%', height: '100%' }}>
<MapContainer
ref={mapRef}
center={[51.505, -0.09]}
zoom={13}
style={{ height: "100%", width: "100%" }}
>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</MapContainer>
</div>
);
}