Skip to content
September 6, 20266 min readBy Dzaki Amri Zaidaan

Sprinkle: Building a Location-Aware Donation Platform with React and Mapbox GL JS

Sprinkle is a frontend-focused donation platform that uses geolocation and interactive maps to connect donors with local charities. This article explores the architecture, real-time data handling, and performance trade-offs of building such a system with React and Mapbox GL JS.

#Frontend
turned-on MacBook Pro

The Problem & Industry Shift

Online donation platforms have traditionally been centralized and impersonal. Donors contribute to large organizations, but often lack visibility into the local impact of their generosity. This disconnect reduces engagement and trust. The industry is shifting towards hyper-local, transparent giving, where donors can see exactly where their contributions go and how they help their own communities.

Sprinkle addresses this by creating a platform that connects donors with nearby charities and donation drives. The core technical challenge is building a responsive, real-time map interface that displays donation opportunities based on the user's location. This requires handling geolocation, rendering dynamic markers, and managing real-time updates without sacrificing performance.

Architecture & Core Mechanics

The frontend is built with React and TypeScript, leveraging Mapbox GL JS for map rendering. The application follows a component-based architecture, with a clear separation between map logic, data fetching, and UI state.

Data Flow

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│  Browser    │────▶│  React App   │────▶│  Mapbox GL  │
│  Geolocation│     │  (State)     │     │  (Render)   │
└─────────────┘     └──────────────┘     └─────────────┘
       │                    │                    │
       ▼                    ▼                    ▼
┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│  User       │     │  API Client  │     │  Map Events │
│  Location   │     │  (fetch)     │     │  (click)    │
└─────────────┘     └──────────────┘     └─────────────┘
       │                    │                    │
       ▼                    ▼                    ▼
┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│  Context    │     │  REST API    │     │  Popup      │
│  Provider   │     │  (Backend)   │     │  Component  │
└─────────────┘     └──────────────┘     └─────────────┘

Key Components

  • MapView: Wraps Mapbox GL, handles map initialization, camera movement, and marker rendering.
  • DonationMarker: Represents a single donation opportunity on the map. It uses useMemo to avoid unnecessary re-renders.
  • LocationContext: Provides the user's current coordinates to any component that needs it, using the Geolocation API.
  • useDonationData: A custom hook that fetches donation data from the backend based on the current map bounds and user location.

Production Code Example

Below is a simplified but realistic example of the MapView component, highlighting critical engineering decisions such as debouncing map moves to prevent API spam, using useRef for the map instance, and cleaning up event listeners.

import { useEffect, useRef, useState } from 'react';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';

mapboxgl.accessToken = process.env.REACT_APP_MAPBOX_TOKEN!;

interface MapViewProps {
  onBoundsChange: (bounds: mapboxgl.LngLatBounds) => void;
}

const MapView: React.FC<MapViewProps> = ({ onBoundsChange }) => {
  const mapContainerRef = useRef<HTMLDivElement>(null);
  const mapRef = useRef<mapboxgl.Map | null>(null);
  const [mapLoaded, setMapLoaded] = useState(false);

  // Debounce bounds change to avoid excessive API calls
  const debouncedBoundsChange = useRef(
    debounce((bounds: mapboxgl.LngLatBounds) => {
      onBoundsChange(bounds);
    }, 300)
  ).current;

  useEffect(() => {
    if (!mapContainerRef.current) return;

    // Initialize map only once
    const map = new mapboxgl.Map({
      container: mapContainerRef.current,
      style: 'mapbox://styles/mapbox/streets-v11',
      center: [-74.5, 40], // Default center
      zoom: 9
    });

    mapRef.current = map;

    map.on('load', () => {
      setMapLoaded(true);
    });

    // Listen to move events and trigger debounced bounds change
    map.on('move', () => {
      const bounds = map.getBounds();
      debouncedBoundsChange(bounds);
    });

    // Clean up on unmount
    return () => {
      map.remove();
      mapRef.current = null;
    };
  }, [debouncedBoundsChange]);

  // Function to add markers (called from parent when data changes)
  const addMarkers = (donations: Donation[]) => {
    if (!mapRef.current) return;

    // Remove existing markers (simplified: use a layer or store markers in ref)
    // ...

    donations.forEach((donation) => {
      const el = document.createElement('div');
      el.className = 'marker';
      el.style.backgroundImage = `url(${donation.icon})`;
      el.style.width = '30px';
      el.style.height = '30px';

      new mapboxgl.Marker(el)
        .setLngLat([donation.lng, donation.lat])
        .setPopup(new mapboxgl.Popup().setHTML(`<h3>${donation.title}</h3><p>${donation.description}</p>`))
        .addTo(mapRef.current);
    });
  };

  return <div ref={mapContainerRef} style={{ width: '100%', height: '100%' }} />;
};

// Debounce utility function
function debounce<T extends (...args: any[]) => void>(func: T, wait: number) {
  let timeout: NodeJS.Timeout;
  return (...args: Parameters<T>) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => func(...args), wait);
  };
}

Critical decisions:

  • Debouncing map moves prevents the backend from being overwhelmed with requests while the user pans.
  • Using useRef for the map instance ensures the map is created only once and persists across re-renders.
  • Cleaning up the map on unmount prevents memory leaks.

Performance, Cost & Trade-offs

Performance Considerations

  • Marker clustering: For areas with many donation opportunities, rendering hundreds of markers can degrade performance. Using Mapbox's clustering feature (via geojson-cluster or the built-in cluster option) reduces the number of DOM elements.
  • Data fetching: Fetching donation data only for the current map bounds reduces payload size. However, frequent bounds changes can cause many requests; debouncing is essential.
  • Real-time updates: If donations are added or removed, consider using WebSockets or Server-Sent Events to update markers without full page reloads. This adds complexity but improves user experience.

Cost Analysis

  • Mapbox API costs: Mapbox charges based on map loads and tile requests. For a small-scale platform, the free tier may suffice, but as usage grows, costs can escalate. Consider using open-source alternatives like Leaflet with OpenStreetMap to reduce costs, but with less out-of-the-box performance.
  • Backend infrastructure: Hosting a REST API and database (e.g., PostgreSQL with PostGIS for geospatial queries) adds cost. Using serverless functions (e.g., AWS Lambda) can be cost-effective for low traffic.

Trade-offs

  • Accuracy vs. performance: Using the user's precise location enables better results but requires permission and may drain battery. A fallback to IP-based location is less accurate but instant.
  • Visual richness vs. load time: Custom map styles and high-resolution tiles improve aesthetics but increase load time. Use vector tiles and optimize images.

Actionable Checklist / Summary

When building a location-aware donation platform, consider the following:

  1. Choose a map library that fits your budget and performance needs. Mapbox GL JS is powerful but has costs; Leaflet is free but less performant for large datasets.
  2. Implement geolocation gracefully: Always handle permission denial and provide a manual location input fallback.
  3. Optimize data fetching: Fetch only the data within the current viewport, debounce map moves, and cache responses.
  4. Use marker clustering for dense areas to maintain smooth interaction.
  5. Consider real-time updates if donation availability changes frequently; use WebSockets for live updates.
  6. Test on low-end devices: Map rendering can be heavy; ensure the UI remains responsive.
  7. Accessibility: Provide alternative list views for users who cannot interact with the map.

By following these practices, you can build a robust, user-friendly donation platform that leverages geolocation to foster local generosity.

References