tessl install tessl/npm-react-leaflet@5.0.3React components for Leaflet maps
Components for organizing and grouping layers together, including support for GeoJSON data and custom rendering panes.
Groups multiple layers together for collective management.
/**
* Component for grouping multiple layers
* @param props - Layer group properties
*/
const LayerGroup: FunctionComponent<LayerGroupProps>;
interface LayerGroupProps extends LayerOptions, EventedProps {
/** Child layers */
children?: ReactNode;
}Usage Example:
import { MapContainer, TileLayer, LayerGroup, Circle, Marker } from "react-leaflet";
function Map() {
return (
<MapContainer center={[51.505, -0.09]} zoom={13}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<LayerGroup>
<Circle center={[51.505, -0.09]} radius={200} />
<Circle center={[51.51, -0.1]} radius={200} />
<Marker position={[51.505, -0.09]} />
</LayerGroup>
</MapContainer>
);
}Extended layer group with shared styling and popup/tooltip support.
/**
* Component for grouping layers with shared styling
* @param props - Feature group properties
*/
const FeatureGroup: FunctionComponent<FeatureGroupProps>;
interface FeatureGroupProps extends LayerGroupProps, PathProps {}Usage Example:
<FeatureGroup pathOptions={{ color: "purple" }}>
<Popup>Shared popup for all features</Popup>
<Circle center={[51.505, -0.09]} radius={200} />
<Circle center={[51.51, -0.1]} radius={200} />
<Polyline positions={[[51.505, -0.09], [51.51, -0.1]]} />
</FeatureGroup>Renders GeoJSON data as map layers.
/**
* Component for rendering GeoJSON data
* @param props - GeoJSON properties
*/
const GeoJSON: FunctionComponent<GeoJSONProps>;
interface GeoJSONProps extends GeoJSONOptions, LayerGroupProps, PathProps {
/** GeoJSON data to render (required) */
data: GeoJsonObject;
}Usage Example:
const geojsonFeature = {
type: "Feature",
properties: {
name: "Sample Location",
popupContent: "This is where the sample happened.",
},
geometry: {
type: "Point",
coordinates: [-0.09, 51.505],
},
};
<GeoJSON data={geojsonFeature} />With Styling Function:
const geojsonData = {
type: "FeatureCollection",
features: [
{
type: "Feature",
properties: { type: "park" },
geometry: {
type: "Polygon",
coordinates: [[[−0.09, 51.505], [−0.1, 51.51], [−0.08, 51.51], [−0.09, 51.505]]],
},
},
],
};
<GeoJSON
data={geojsonData}
style={(feature) => {
switch (feature.properties.type) {
case "park":
return { color: "green", fillColor: "lightgreen" };
case "water":
return { color: "blue", fillColor: "lightblue" };
default:
return { color: "gray" };
}
}}
/>With Point Styling and Interaction:
<GeoJSON
data={geojsonData}
pointToLayer={(feature, latlng) => {
return L.circleMarker(latlng, { radius: 8, fillColor: "red" });
}}
onEachFeature={(feature, layer) => {
if (feature.properties && feature.properties.name) {
layer.bindPopup(feature.properties.name);
}
}}
/>Custom rendering pane for controlling z-index and rendering order.
/**
* Component for creating custom rendering panes
* @param props - Pane properties
* @param ref - Optional ref to access the HTML element
*/
const Pane: ForwardRefExoticComponent<PaneProps & RefAttributes<PaneRef>>;
interface PaneProps {
/** Pane name (required) */
name: string;
/** Child layers to render in this pane */
children?: ReactNode;
/** CSS class for the pane */
className?: string;
/** Inline styles for the pane */
style?: CSSProperties;
/** Parent pane name */
pane?: string;
}
type PaneRef = HTMLElement | null;Usage Example:
<MapContainer center={[51.505, -0.09]} zoom={13}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<Pane name="custom-pane" style={{ zIndex: 650 }}>
<Circle center={[51.505, -0.09]} radius={200} />
<Marker position={[51.51, -0.1]} />
</Pane>
</MapContainer>Nested Panes:
<Pane name="base-pane" style={{ zIndex: 400 }}>
<Pane name="nested-pane" style={{ zIndex: 450 }}>
<Marker position={[51.505, -0.09]} />
</Pane>
</Pane>// From Leaflet
interface LayerOptions {
/** Map pane where the layer will be added */
pane?: string;
/** Attribution text */
attribution?: string;
}interface PathProps extends InteractiveLayerProps {
/** Path styling options */
pathOptions?: PathOptions;
}// From Leaflet
interface GeoJSONOptions extends LayerOptions {
/** Function to convert Point features to Leaflet layers */
pointToLayer?: (geoJsonPoint: Feature<Point>, latlng: LatLng) => Layer;
/** Function to style features */
style?: StyleFunction;
/** Function called on each created feature layer */
onEachFeature?: (feature: Feature, layer: Layer) => void;
/** Function to filter features */
filter?: (geoJsonFeature: Feature) => boolean;
/** Coordinate system for GeoJSON coordinates */
coordsToLatLng?: (coords: [number, number] | [number, number, number]) => LatLng;
/** Whether to make GeoJSON layers interactive */
markersInheritOptions?: boolean;
}
type StyleFunction = (feature?: Feature) => PathOptions;// From GeoJSON spec
type GeoJsonObject =
| Point
| MultiPoint
| LineString
| MultiLineString
| Polygon
| MultiPolygon
| GeometryCollection
| Feature
| FeatureCollection;
interface Feature<G = Geometry, P = any> {
type: "Feature";
geometry: G;
properties: P;
id?: string | number;
}
interface FeatureCollection<G = Geometry, P = any> {
type: "FeatureCollection";
features: Array<Feature<G, P>>;
}
// Coordinate order in GeoJSON: [longitude, latitude]
// Note: This is opposite of Leaflet's [latitude, longitude]
type GeoJSONPosition = [number, number] | [number, number, number];LayerGroup vs FeatureGroup:
Conditional Rendering: Toggle layer groups visibility:
{showMarkers && (
<LayerGroup>
<Marker position={pos1} />
<Marker position={pos2} />
</LayerGroup>
)}GeoJSON Data Updates: GeoJSON component re-renders when data prop changes:
<GeoJSON data={currentGeoJsonData} />GeoJSON Styling: Use style prop for dynamic styling based on feature properties:
style={(feature) => ({
color: feature.properties.color,
weight: feature.properties.importance * 2,
})}Point Customization: Use pointToLayer to customize how Point features are rendered:
pointToLayer={(feature, latlng) => {
return L.marker(latlng, {
icon: getIconForFeature(feature),
});
}}Feature Interaction: Use onEachFeature to add popups, tooltips, or event handlers:
onEachFeature={(feature, layer) => {
layer.bindPopup(feature.properties.name);
layer.on({
click: () => console.log(feature.properties),
});
}}GeoJSON Filtering: Filter which features to display:
filter={(feature) => feature.properties.visible === true}Pane Z-Index: Default pane z-indexes:
Custom Pane Styling: Control layer rendering order with custom panes:
<Pane name="labels" style={{ zIndex: 650 }}>
{/* Layers that should appear above others */}
</Pane>function MapWithTogglableLayers() {
const [showMarkers, setShowMarkers] = useState(true);
const [showCircles, setShowCircles] = useState(true);
return (
<>
<button onClick={() => setShowMarkers(!showMarkers)}>
Toggle Markers
</button>
<button onClick={() => setShowCircles(!showCircles)}>
Toggle Circles
</button>
<MapContainer center={[51.505, -0.09]} zoom={13}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
{showMarkers && (
<LayerGroup>
<Marker position={[51.505, -0.09]} />
<Marker position={[51.51, -0.1]} />
</LayerGroup>
)}
{showCircles && (
<LayerGroup>
<Circle center={[51.505, -0.09]} radius={200} />
<Circle center={[51.51, -0.1]} radius={200} />
</LayerGroup>
)}
</MapContainer>
</>
);
}function GeoJSONMap({ geojsonData }) {
return (
<MapContainer center={[51.505, -0.09]} zoom={13}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<GeoJSON
data={geojsonData}
style={(feature) => {
const magnitude = feature.properties.magnitude;
return {
color: magnitude > 5 ? "red" : magnitude > 3 ? "orange" : "yellow",
weight: magnitude,
fillOpacity: 0.5,
};
}}
onEachFeature={(feature, layer) => {
layer.bindPopup(
`<h3>${feature.properties.name}</h3><p>Magnitude: ${feature.properties.magnitude}</p>`
);
}}
/>
</MapContainer>
);
}import L from "leaflet";
function GeoJSONWithCustomMarkers({ pointsData }) {
return (
<GeoJSON
data={pointsData}
pointToLayer={(feature, latlng) => {
const icon = L.divIcon({
className: "custom-marker",
html: `<div style="background-color: ${feature.properties.color}; width: 20px; height: 20px; border-radius: 50%;"></div>`,
});
return L.marker(latlng, { icon });
}}
onEachFeature={(feature, layer) => {
layer.bindTooltip(feature.properties.label, { permanent: true });
}}
/>
);
}function MultiPaneMap() {
return (
<MapContainer center={[51.505, -0.09]} zoom={13}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<Pane name="background-shapes" style={{ zIndex: 350 }}>
<Circle center={[51.505, -0.09]} radius={500} pathOptions={{ color: "blue" }} />
</Pane>
<Pane name="foreground-markers" style={{ zIndex: 650 }}>
<Marker position={[51.505, -0.09]} />
</Pane>
<Pane name="labels" style={{ zIndex: 700 }}>
<Tooltip position={[51.505, -0.09]} permanent>
Important Label
</Tooltip>
</Pane>
</MapContainer>
);
}function StyledFeatureGroup() {
return (
<FeatureGroup
pathOptions={{
color: "purple",
weight: 3,
fillColor: "lavender",
fillOpacity: 0.5,
}}
>
<Popup>These features share styling</Popup>
<Circle center={[51.505, -0.09]} radius={200} />
<Circle center={[51.51, -0.1]} radius={200} />
<Rectangle bounds={[[51.49, -0.08], [51.50, -0.06]]} />
</FeatureGroup>
);
}function MultiPaneLayerGroups() {
return (
<MapContainer center={[51.505, -0.09]} zoom={13}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<Pane name="background" style={{ zIndex: 400 }}>
<LayerGroup>
<Circle center={[51.505, -0.09]} radius={500} pathOptions={{ color: "blue" }} />
</LayerGroup>
</Pane>
<Pane name="foreground" style={{ zIndex: 600 }}>
<LayerGroup>
<Marker position={[51.505, -0.09]} />
</LayerGroup>
</Pane>
</MapContainer>
);
}function FilteredGeoJSON({ data, filter }) {
const filteredData = useMemo(() => ({
...data,
features: data.features.filter(feature =>
feature.properties.category === filter
),
}), [data, filter]);
return (
<GeoJSON
data={filteredData}
style={(feature) => ({
color: feature.properties.color || "blue",
weight: 2,
fillOpacity: 0.5,
})}
onEachFeature={(feature, layer) => {
layer.bindPopup(`<h3>${feature.properties.name}</h3>`);
}}
/>
);
}function AnimatedGeoJSON({ data }) {
const [visibleFeatures, setVisibleFeatures] = useState([]);
useEffect(() => {
let index = 0;
const interval = setInterval(() => {
if (index < data.features.length) {
setVisibleFeatures(prev => [...prev, data.features[index]]);
index++;
} else {
clearInterval(interval);
}
}, 500);
return () => clearInterval(interval);
}, [data]);
const animatedData = useMemo(() => ({
type: "FeatureCollection",
features: visibleFeatures,
}), [visibleFeatures]);
return <GeoJSON data={animatedData} />;
}function ClusteredGeoJSON({ data, zoom }) {
const processedData = useMemo(() => {
if (zoom > 12) return data; // Show all points at high zoom
// Simple clustering logic
const clusters = new Map();
data.features.forEach(feature => {
if (feature.geometry.type === "Point") {
const [lng, lat] = feature.geometry.coordinates;
const key = `${Math.floor(lat)},${Math.floor(lng)}`;
if (!clusters.has(key)) {
clusters.set(key, {
type: "Feature",
geometry: { type: "Point", coordinates: [lng, lat] },
properties: { count: 0, items: [] },
});
}
const cluster = clusters.get(key);
cluster.properties.count++;
cluster.properties.items.push(feature.properties);
}
});
return {
type: "FeatureCollection",
features: Array.from(clusters.values()),
};
}, [data, zoom]);
return (
<GeoJSON
data={processedData}
pointToLayer={(feature, latlng) => {
const count = feature.properties.count;
return L.circleMarker(latlng, {
radius: Math.min(count * 5, 50),
fillColor: count > 10 ? "red" : "blue",
fillOpacity: 0.7,
});
}}
onEachFeature={(feature, layer) => {
layer.bindPopup(`${feature.properties.count} items`);
}}
/>
);
}function ConditionalLayers({ showMarkers, showCircles, showPolygons }) {
return (
<>
{showMarkers && (
<LayerGroup>
<Marker position={[51.505, -0.09]} />
<Marker position={[51.51, -0.1]} />
</LayerGroup>
)}
{showCircles && (
<FeatureGroup pathOptions={{ color: "blue" }}>
<Circle center={[51.505, -0.09]} radius={200} />
<Circle center={[51.51, -0.1]} radius={200} />
</FeatureGroup>
)}
{showPolygons && (
<LayerGroup>
<Polygon positions={[[51.515, -0.09], [51.52, -0.1], [51.52, -0.12]]} />
</LayerGroup>
)}
</>
);
}function LazyGeoJSON({ url }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
});
}, [url]);
if (loading) return <div>Loading...</div>;
if (!data) return null;
return <GeoJSON data={data} />;
}const MemoizedLayerGroup = React.memo(({ children, pane }) => (
<LayerGroup pane={pane}>
{children}
</LayerGroup>
));
function OptimizedLayers({ layers }) {
return (
<>
{layers.map(layer => (
<MemoizedLayerGroup key={layer.id} pane={layer.pane}>
{layer.content}
</MemoizedLayerGroup>
))}
</>
);
}Solutions:
Solutions:
Solutions:
import { render } from '@testing-library/react';
import { MapContainer, GeoJSON } from 'react-leaflet';
const testGeoJSON = {
type: "FeatureCollection",
features: [{
type: "Feature",
geometry: { type: "Point", coordinates: [-0.09, 51.505] },
properties: { name: "Test Point" }
}]
};
test('renders GeoJSON features', () => {
const { container } = render(
<MapContainer center={[51.505, -0.09]} zoom={13}>
<GeoJSON data={testGeoJSON} />
</MapContainer>
);
expect(container).toBeInTheDocument();
});test('toggles layer group visibility', () => {
const { rerender } = render(
<MapContainer center={[51.505, -0.09]} zoom={13}>
{true && (
<LayerGroup>
<Marker position={[51.505, -0.09]} />
</LayerGroup>
)}
</MapContainer>
);
// Test visibility toggle
});// Leaflet uses [latitude, longitude]
const leafletCoords: LatLngExpression = [51.505, -0.09];
// GeoJSON uses [longitude, latitude]
const geoJSONCoords: GeoJSONPosition = [-0.09, 51.505];
// Convert GeoJSON to Leaflet
const geoJsonData = {
type: "Feature",
geometry: {
type: "Point",
coordinates: [-0.09, 51.505] // [lng, lat] in GeoJSON
}
};
// GeoJSON component handles conversion automatically
<GeoJSON data={geoJsonData} /><GeoJSON
data={geojsonData}
onEachFeature={(feature, layer) => {
if (feature.properties && feature.properties.name) {
layer.bindPopup(
`<div role="region" aria-label="${feature.properties.name}">
<h3>${feature.properties.name}</h3>
<p>${feature.properties.description}</p>
</div>`
);
}
}}
/>