@silicon.js/map
Version:
A React Native map management package built on top of `react-native-maps` for Expo with a powerful provider-based architecture for managing regions, markers, and selections.
678 lines (519 loc) β’ 15.5 kB
Markdown
A React Native map management package built on top of `react-native-maps` for Expo with a powerful provider-based architecture for managing regions, markers, and selections.
- πΊοΈ Easy map region management
- π Full marker CRUD operations (Create, Read, Update, Delete)
- π― Marker selection with visual feedback
- π± User location tracking
- π¨ Custom marker icons support
- π Automatic state synchronization
- πΎ Centralized map state management
- π TypeScript support
- β‘ Optimized with memoized callbacks
- π¦ Expo-compatible
## Installation
```bash
npx expo install react-native-maps
```
## Setup
### 1. Configure app.json/app.config.js
Add the maps plugin to your Expo config:
```json
{
"expo": {
"plugins": [
[
"expo-location",
{
"locationAlwaysAndWhenInUsePermission": "Allow $(PRODUCT_NAME) to use your location."
}
]
],
"ios": {
"bundleIdentifier": "com.yourcompany.yourapp",
"config": {
"googleMapsApiKey": "YOUR_IOS_API_KEY"
}
},
"android": {
"package": "com.yourcompany.yourapp",
"config": {
"googleMaps": {
"apiKey": "YOUR_ANDROID_API_KEY"
}
}
}
}
}
```
```bash
npx expo install expo-location
```
```tsx
import { MapProvider } from './map';
function App() {
return <MapProvider>{/* Your app components */}</MapProvider>;
}
```
```tsx
import { Map, useMapContext } from './map';
function MapScreen() {
const { addMarker } = useMapContext();
const handleAddMarker = () => {
addMarker({
id: '1',
latitude: 37.78825,
longitude: -122.4324,
title: 'San Francisco',
description: 'City by the Bay',
});
};
return (
<View style={{ flex: 1 }}>
<Map
initialRegion={{
latitude: 37.78825,
longitude: -122.4324,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
}}
showsUserLocation
style={{ flex: 1 }}
/>
<Button title="Add Marker" onPress={handleAddMarker} />
</View>
);
}
```
Wrap your app to provide map context to all child components.
```tsx
<MapProvider>{children}</MapProvider>
```
| Prop | Type | Required | Description |
| --------------------- | ---------------------------- | -------- | ----------------------------------------------- |
| `initialRegion` | `MapRegion` | No | Initial map region (lat, lng, deltas) |
| `showsUserLocation` | `boolean` | No | Show user's current location (default: `false`) |
| `followsUserLocation` | `boolean` | No | Center map on user location (default: `false`) |
| `style` | `ViewStyle` | No | Custom styles for the map container |
| `onMarkerPress` | `(markerId: string) => void` | No | Callback when marker is pressed |
Access map state and methods throughout your app.
```typescript
interface MapState {
region: MapRegion | null;
markers: MapMarker[];
selectedMarkerId: string | null;
}
```
**Region Management**
- `setRegion(region: MapRegion): void` - Update map viewport
**Marker Management**
- `setMarkers(markers: MapMarker[]): void` - Replace all markers
- `addMarker(marker: MapMarker): void` - Add a single marker
- `removeMarker(markerId: string): void` - Remove marker by ID
- `updateMarker(markerId: string, updates: Partial<MapMarker>): void` - Update marker properties
- `clearMarkers(): void` - Remove all markers
**Selection Management**
- `selectMarker(markerId: string | null): void` - Select/deselect marker
```typescript
interface MapRegion {
latitude: number;
longitude: number;
latitudeDelta?: number;
longitudeDelta?: number;
}
```
```typescript
interface MapMarker {
id: string;
latitude: number;
longitude: number;
title?: string;
description?: string;
draggable?: boolean;
icon?: ReactNode;
}
```
```tsx
import { Map } from './map';
function SimpleMap() {
return (
<Map
initialRegion={{
latitude: 37.78825,
longitude: -122.4324,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
}}
showsUserLocation
style={{ flex: 1 }}
/>
);
}
```
```tsx
import { useMapContext } from './map';
function LocationPicker() {
const { addMarker, mapState } = useMapContext();
const handleMapPress = (coordinate) => {
const newMarker = {
id: Date.now().toString(),
latitude: coordinate.latitude,
longitude: coordinate.longitude,
title: 'New Location',
};
addMarker(newMarker);
};
return (
<View>
<Text>Total Markers: {mapState.markers.length}</Text>
{/* Map component */}
</View>
);
}
```
```tsx
import { useMapContext } from './map';
function LocationManager() {
const { setMarkers, clearMarkers, mapState } = useMapContext();
const loadLocations = async () => {
const locations = await fetchLocations();
const markers = locations.map((loc) => ({
id: loc.id,
latitude: loc.lat,
longitude: loc.lng,
title: loc.name,
description: loc.address,
}));
setMarkers(markers);
};
return (
<View>
<Button title="Load Locations" onPress={loadLocations} />
<Button title="Clear All" onPress={clearMarkers} />
<Text>{mapState.markers.length} locations</Text>
</View>
);
}
```
```tsx
import { useMapContext, Map } from './map';
function InteractiveMap() {
const { mapState, selectMarker } = useMapContext();
const selectedMarker = mapState.markers.find((m) => m.id === mapState.selectedMarkerId);
return (
<View style={{ flex: 1 }}>
<Map
showsUserLocation
style={{ flex: 1 }}
onMarkerPress={(markerId) => {
selectMarker(markerId);
}}
/>
{selectedMarker && (
<View style={styles.detailsPanel}>
<Text style={styles.title}>{selectedMarker.title}</Text>
<Text>{selectedMarker.description}</Text>
<Button title="Close" onPress={() => selectMarker(null)} />
</View>
)}
</View>
);
}
```
```tsx
import { useMapContext } from './map';
function EditableMarkers() {
const { updateMarker, mapState } = useMapContext();
const handleEditMarker = (markerId: string) => {
updateMarker(markerId, {
title: 'Updated Title',
description: 'New description',
draggable: true,
});
};
return (
<View>
{mapState.markers.map((marker) => (
<TouchableOpacity key={marker.id} onPress={() => handleEditMarker(marker.id)}>
<Text>{marker.title}</Text>
</TouchableOpacity>
))}
</View>
);
}
```
```tsx
import { useMapContext } from './map';
import { Image } from 'react-native';
function CustomMarkerMap() {
const { addMarker } = useMapContext();
const addCustomMarker = () => {
addMarker({
id: 'custom-1',
latitude: 37.78825,
longitude: -122.4324,
title: 'Custom Marker',
icon: <Image source={require('./pin.png')} style={{ width: 30, height: 30 }} />,
});
};
return <Button title="Add Custom Marker" onPress={addCustomMarker} />;
}
```
```tsx
import { useMapContext } from './map';
function RegionControls() {
const { setRegion } = useMapContext();
const centerOnSF = () => {
setRegion({
latitude: 37.78825,
longitude: -122.4324,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
});
};
const centerOnNY = () => {
setRegion({
latitude: 40.7128,
longitude: -74.006,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
});
};
return (
<View>
<Button title="San Francisco" onPress={centerOnSF} />
<Button title="New York" onPress={centerOnNY} />
</View>
);
}
```
```tsx
import { useMapContext } from './map';
import * as Location from 'expo-location';
function UserLocationMap() {
const { setRegion, addMarker } = useMapContext();
useEffect(() => {
(async () => {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
alert('Permission to access location was denied');
return;
}
const location = await Location.getCurrentPositionAsync({});
const { latitude, longitude } = location.coords;
// Center map on user
setRegion({
latitude,
longitude,
latitudeDelta: 0.01,
longitudeDelta: 0.01,
});
// Add user marker
addMarker({
id: 'user',
latitude,
longitude,
title: 'You are here',
});
})();
}, []);
return <Map showsUserLocation style={{ flex: 1 }} />;
}
```
```tsx
import { useMapContext } from './map';
function FilteredMap() {
const { mapState, setMarkers } = useMapContext();
const [category, setCategory] = useState('all');
const filterByCategory = (cat: string) => {
setCategory(cat);
const filtered = allMarkers.filter((m) => cat === 'all' || m.category === cat);
setMarkers(filtered);
};
return (
<View>
<Button title="All" onPress={() => filterByCategory('all')} />
<Button title="Restaurants" onPress={() => filterByCategory('restaurant')} />
<Button title="Hotels" onPress={() => filterByCategory('hotel')} />
</View>
);
}
```
```tsx
import { useMapContext } from './map';
function RouteMap() {
const { setMarkers, mapState } = useMapContext();
const planRoute = (waypoints: Coordinate[]) => {
const markers = waypoints.map((point, index) => ({
id: `waypoint-${index}`,
latitude: point.latitude,
longitude: point.longitude,
title: `Stop ${index + 1}`,
draggable: true,
}));
setMarkers(markers);
};
return <Map style={{ flex: 1 }} />;
}
```
```tsx
import { useMapContext } from './map';
import * as Location from 'expo-location';
function LocationTracker() {
const { updateMarker, setRegion } = useMapContext();
useEffect(() => {
let subscription;
(async () => {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') return;
subscription = await Location.watchPositionAsync(
{
accuracy: Location.Accuracy.High,
timeInterval: 1000,
distanceInterval: 10,
},
(location) => {
const { latitude, longitude } = location.coords;
updateMarker('user-location', { latitude, longitude });
setRegion({
latitude,
longitude,
latitudeDelta: 0.01,
longitudeDelta: 0.01,
});
},
);
})();
return () => subscription?.remove();
}, []);
return <Map followsUserLocation style={{ flex: 1 }} />;
}
```
Display multiple store locations with search and filtering
Show real-time delivery driver location and route
Display real estate properties on a map with details
Show nearby events with custom markers
Create multi-stop routes with draggable waypoints
Allow users to select locations by tapping the map
Selected markers are highlighted (opacity: 1.0) while unselected markers are dimmed (opacity: 0.5), providing clear visual feedback.
### Draggable Markers
```tsx
addMarker({
id: 'draggable-1',
latitude: 37.78825,
longitude: -122.4324,
draggable: true, // Enable dragging
});
```
- **Memoized callbacks** - All context methods use `useCallback` to prevent unnecessary re-renders
- **Efficient state updates** - State updates are batched and optimized
- **Selective rendering** - Only affected markers re-render on updates
```bash
npx expo install expo-location
```
```tsx
import * as Location from 'expo-location';
const { status } = await Location.requestForegroundPermissionsAsync();
const location = await Location.getCurrentPositionAsync({});
```
For optimal performance and full feature support, use EAS Build or development builds:
```bash
npx expo run:ios
npx expo run:android
```
When using Expo Go:
- Limited map styling options
- Some native features may not work
- Consider using development builds for production apps
1. **Always provide unique IDs** - Use UUIDs or timestamps for marker IDs
2. **Handle permissions** - Request location permissions before enabling user location
3. **Optimize marker count** - Consider clustering for 100+ markers
4. **Clean up on unmount** - Clear markers when navigating away
5. **Test on real devices** - Maps behave differently on simulators
6. **Handle loading states** - Show indicators while fetching location data
7. **Provide fallbacks** - Handle cases where location services are disabled
8. **Use development builds** - Expo Go has limitations; use dev builds for full features
- Requires location permissions in `app.json`
- Native Apple Maps integration
- Better performance with many markers
- Requires Google Maps API key in `app.json`
- Google Maps integration
- May require Google Play Services
- Check API key configuration in `app.json`
- Verify permissions are granted
- Ensure region has valid coordinates
- Try using a development build instead of Expo Go
- Verify marker coordinates are within visible region
- Check that markers array is not empty
- Ensure marker IDs are unique
- Reduce number of visible markers
- Implement marker clustering
- Optimize custom marker icons
- Use development builds for better performance
```tsx
import * as Location from 'expo-location';
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
Alert.alert('Permission denied', 'Location permission is required');
}
```
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project or select existing
3. Enable Maps SDK for Android and iOS
4. Create credentials (API Key)
5. Add to `app.json`
## TypeScript Support
Fully typed with TypeScript for excellent IDE support and type safety:
```typescript
const { mapState, addMarker }: MapContextType = useMapContext();
```