npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@mapfirst.ai/react

v0.0.19

Published

React hooks for MapFirst SDK - Reactive state management for map properties

Readme

@mapfirst/react

React hooks and components for the MapFirst SDK supporting MapLibre, Google Maps, and Mapbox.

Features

  • 🗺️ Multi-Platform Support: Works with MapLibre GL JS, Google Maps, and Mapbox GL JS
  • 🔍 SmartFilter Component: AI-powered search with interactive filter chips
  • ⚛️ React Hooks: Reactive state management for properties, filters, and map state
  • 🎨 Customizable: Native React styles (CSS-in-JS) - no framework dependencies
  • Accessible: Full keyboard navigation and ARIA support
  • 🌍 i18n Ready: Built-in translations with extensibility

Installation

npm install @mapfirst/react @mapfirst/core
# or
pnpm add @mapfirst/react @mapfirst/core
# or
yarn add @mapfirst/react @mapfirst/core

Quick Start - SmartFilter Component

import { useMapFirst, SmartFilter } from "@mapfirst/react";
import { useState } from "react";

function App() {
  const {
    instance: mapFirst,
    state,
    smartFilterSearch,
  } = useMapFirst({
    initialLocationData: {
      city: "New York",
      country: "United States",
      currency: "USD",
    },
  });

  const [filters, setFilters] = useState([]);

  return (
    <SmartFilter
      mapFirst={mapFirst}
      filters={filters}
      isSearching={state?.isSearching}
      onSearch={async (query) => {
        await smartFilterSearch.search({ query });
      }}
      onFilterChange={setFilters}
    />
  );
}

See SMARTFILTER.md for complete SmartFilter documentation.

Usage

The React SDK supports a two-phase initialization pattern:

  1. Create the MapFirst SDK instance (optionally with location data)
  2. Attach your map when it's ready

MapLibre GL JS

import React, { useEffect, useRef, useState } from "react";
import maplibregl from "maplibre-gl";
import { useMapFirst } from "@mapfirst/react";
import "maplibre-gl/dist/maplibre-gl.css";

function MapLibreExample() {
  const mapContainerRef = useRef<HTMLDivElement>(null);
  const [map, setMap] = useState<maplibregl.Map | null>(null);

  // Phase 1: Create SDK instance with location data
  const {
    instance: mapFirst,
    state,
    attachMapLibre,
    smartFilterSearch,
    boundsSearch,
  } = useMapFirst({
    initialLocationData: {
      city: "Paris",
      country: "France",
      currency: "EUR",
    },
    environment: "prod",
    mfid: "your-mfid",
  });

  // Access reactive state
  const properties = state?.properties || [];
  const isSearching = state?.isSearching || false;

  // Initialize MapLibre map
  useEffect(() => {
    if (!mapContainerRef.current) return;

    const mapInstance = new maplibregl.Map({
      container: mapContainerRef.current,
      style: "https://demotiles.maplibre.org/style.json",
      center: [2.3522, 48.8566], // Paris
      zoom: 12,
    });

    mapInstance.on("load", () => {
      setMap(mapInstance);
    });

    return () => {
      mapInstance.remove();
    };
  }, []);

  // Phase 2: Attach map to SDK
  useEffect(() => {
    if (map && mapFirst) {
      attachMapLibre(map, maplibregl, {
        onMarkerClick: (marker) => {
          console.log("Marker clicked:", marker);
        },
      });
    }
  }, [map, mapFirst, attachMapLibre]);

  return (
    <div ref={mapContainerRef} style={{ width: "100%", height: "600px" }} />
  );
}

Google Maps

import React, { useEffect, useRef, useState } from "react";
import { useMapFirst } from "@mapfirst/react";

function GoogleMapsExample() {
  const mapContainerRef = useRef<HTMLDivElement>(null);
  const [map, setMap] = useState<google.maps.Map | null>(null);

  // Phase 1: Create SDK instance
  const {
    instance: mapFirst,
    state,
    attachGoogle,
  } = useMapFirst({
    initialLocationData: {
      city: "Tokyo",
      country: "Japan",
      currency: "JPY",
    },
  });

  // Initialize Google Maps
  useEffect(() => {
    if (!mapContainerRef.current || !window.google) return;

    const mapInstance = new google.maps.Map(mapContainerRef.current, {
      center: { lat: 35.6762, lng: 139.6503 }, // Tokyo
      zoom: 12,
      mapId: "your-map-id", // Required for Advanced Markers
    });

    setMap(mapInstance);
  }, []);

  // Phase 2: Attach map to SDK
  useEffect(() => {
    if (map && mapFirst) {
      attachGoogle(map, window.google, {
        onMarkerClick: (marker) => {
          console.log("Marker clicked:", marker);
        },
      });
    }
  }, [map, mapFirst, attachGoogle]);

  return (
    <div ref={mapContainerRef} style={{ width: "100%", height: "600px" }} />
  );
}

Mapbox GL JS

import React, { useEffect, useRef, useState } from "react";
import mapboxgl from "mapbox-gl";
import { useMapFirst } from "@mapfirst/react";
import "mapbox-gl/dist/mapbox-gl.css";

function MapboxExample() {
  const mapContainerRef = useRef<HTMLDivElement>(null);
  const [map, setMap] = useState<mapboxgl.Map | null>(null);

  // Phase 1: Create SDK instance
  const {
    instance: mapFirst,
    state,
    attachMapbox,
  } = useMapFirst({
    initialLocationData: {
      city: "London",
      country: "United Kingdom",
      currency: "GBP",
    },
  });

  // Initialize Mapbox map
  useEffect(() => {
    if (!mapContainerRef.current) return;

    mapboxgl.accessToken = "your-mapbox-token";

    const mapInstance = new mapboxgl.Map({
      container: mapContainerRef.current,
      style: "mapbox://styles/mapbox/streets-v12",
      center: [-0.1276, 51.5074], // London
      zoom: 12,
    });

    mapInstance.on("load", () => {
      setMap(mapInstance);
    });

    return () => {
      mapInstance.remove();
    };
  }, []);

  // Phase 2: Attach map to SDK
  useEffect(() => {
    if (map && mapFirst) {
      attachMapbox(map, mapboxgl, {
        onMarkerClick: (marker) => {
          console.log("Marker clicked:", marker);
        },
      });
    }
  }, [map, mapFirst, attachMapbox]);

  return (
    <div ref={mapContainerRef} style={{ width: "100%", height: "600px" }} />
  );
}

Advanced Usage

Accessing SDK Methods and Reactive State

All SDK methods are available through the instance, and state updates automatically trigger React re-renders:

const {
  instance: mapFirst,
  state,
  setPrimaryType,
  setSelectedMarker,
  propertiesSearch,
  smartFilterSearch,
  boundsSearch,
} = useMapFirst({
  /* ... */
});

// Access reactive state (automatically updates)
const properties = state?.properties || [];
const isSearching = state?.isSearching || false;
const selectedId = state?.selectedPropertyId;
const filters = state?.filters;

// Fly to location
useEffect(() => {
  if (mapFirst) {
    mapFirst.flyMapTo(2.3522, 48.8566, 14); // lng, lat, zoom
  }
}, [mapFirst]);

// Run properties search using the hook's search method
const handleSearch = async () => {
  await propertiesSearch.search({
    body: {
      city: "Paris",
      country: "France",
      filters: {
        checkIn: "2024-06-01",
        checkOut: "2024-06-07",
        numAdults: 2,
        currency: "EUR",
      },
    },
  });
};

// Run smart filter search
const handleSmartSearch = async (query: string) => {
  await smartFilterSearch.search({ query });
};

// Perform bounds search
const handleBoundsSearch = async () => {
  await boundsSearch.perform();
};

// Set primary type
setPrimaryType("Restaurant");

// Set selected marker
setSelectedMarker(123456);

// Set filters directly on instance
mapFirst?.setFilters({
  checkIn: new Date("2024-06-01"),
  checkOut: new Date("2024-06-07"),
  numAdults: 2,
  currency: "EUR",
});

Using Search Methods

The unified hook provides search methods with their own loading states:

const {
  instance: mapFirst,
  state,
  propertiesSearch,
  smartFilterSearch,
  boundsSearch,
} = useMapFirst({
  /* ... */
});

// Properties search with loading state
const handlePropertiesSearch = async () => {
  try {
    await propertiesSearch.search({
      body: {
        city: "Paris",
        country: "France",
        filters: {
          checkIn: new Date(),
          checkOut: new Date(Date.now() + 86400000 * 3),
          numAdults: 2,
        },
      },
    });
  } catch (err) {
    console.error("Search failed:", propertiesSearch.error);
  }
};

// Smart filter search with loading state
const handleSmartSearch = async (query: string) => {
  try {
    await smartFilterSearch.search({ query });
  } catch (err) {
    console.error("Search failed:", smartFilterSearch.error);
  }
};

// Bounds search with loading state
const handleBoundsSearch = async () => {
  if (!state?.pendingBounds) return;

  try {
    await boundsSearch.perform();
  } catch (err) {
    console.error("Search failed:", boundsSearch.error);
  }
};

return (
  <div>
    <button
      onClick={handlePropertiesSearch}
      disabled={propertiesSearch.isLoading}
    >
      {propertiesSearch.isLoading ? "Searching..." : "Search Properties"}
    </button>

    <button
      onClick={() => handleSmartSearch("hotels with pool")}
      disabled={smartFilterSearch.isLoading}
    >
      {smartFilterSearch.isLoading ? "Searching..." : "Smart Search"}
    </button>

    <button
      onClick={handleBoundsSearch}
      disabled={boundsSearch.isSearching || !state?.pendingBounds}
    >
      {boundsSearch.isSearching ? "Searching..." : "Search This Area"}
    </button>
  </div>
);

State Management with Callbacks

const { instance: mapFirst, state } = useMapFirst({
  initialLocationData: {
    city: "Paris",
    country: "France",
  },
  callbacks: {
    onPropertiesChange: (properties) => {
      console.log("Properties updated:", properties.length);
    },
    onSelectedPropertyChange: (id) => {
      console.log("Selected property:", id);
    },
    onError: (error, context) => {
      console.error(`Error in ${context}:`, error);
    },
    onLoadingStateChange: (loading) => {
      console.log("Loading:", loading);
    },
  },
});

Configuring Map Behavior

const { instance: mapFirst, state } = useMapFirst({
  initialLocationData: {
    city: "New York",
    country: "United States",
  },
  fitBoundsPadding: {
    top: 100,
    bottom: 200,
    left: 50,
    right: 50,
  },
  properties: [], // Initial properties
  primaryType: "Accommodation", // or 'Restaurant', 'Attraction'
  autoSelectOnClick: true, // Auto-select marker on click
  environment: "prod", // or 'test'
});

API Reference

useMapFirst(options)

Comprehensive hook for MapFirst SDK with all functionality in one place. Creates a MapFirstCore instance with reactive state and provides all necessary methods.

Returns:

{
  instance: MapFirstCore | null,
  state: MapState | null,
  setPrimaryType: (type: PropertyType) => void,
  setSelectedMarker: (id: number | null) => void,
  propertiesSearch: {
    search: (options) => Promise<any>,
    isLoading: boolean,
    error: Error | null
  },
  smartFilterSearch: {
    search: (options) => Promise<any>,
    isLoading: boolean,
    error: Error | null
  },
  boundsSearch: {
    perform: () => Promise<any>,
    isSearching: boolean,
    error: Error | null
  },
  attachMapLibre: (map, maplibregl, options?) => void,
  attachGoogle: (map, google, options?) => void,
  attachMapbox: (map, mapboxgl, options?) => void
}
  • instance - The SDK instance for calling methods
  • state - Reactive state that updates when SDK state changes
    • properties - Array of properties
    • selectedPropertyId - Currently selected property ID
    • primary - Primary property type
    • filters - Current filter state
    • isSearching - Whether a search is in progress
    • initialLoading - Whether initial data is loading
    • bounds - Current map bounds
    • center - Map center coordinates
    • zoom - Current zoom level
    • activeLocation - Active location data
  • setPrimaryType - Function to change the primary property type
  • setSelectedMarker - Function to select/deselect markers
  • propertiesSearch - Object with search method, loading state, and error
  • smartFilterSearch - Object with search method, loading state, and error
  • boundsSearch - Object with perform method, searching state, and error
  • attachMapLibre - Function to attach MapLibre map
  • attachGoogle - Function to attach Google Maps
  • attachMapbox - Function to attach Mapbox map

Options:

  • initialLocationData? - Location data for geo-lookup
    • city? - City name
    • country? - Country name
    • query? - Search query
    • currency? - Currency code
  • environment? - API environment ('prod' | 'test')
  • mfid? - MapFirst ID
  • state? - Initial map state
  • callbacks? - State change callbacks
  • fitBoundsPadding? - Padding for fitBounds operations
  • properties? - Initial properties
  • primaryType? - Primary property type
  • autoSelectOnClick? - Auto-select markers on click

Map Attachment Methods

The hook returns three functions for attaching different map types:

attachMapLibre(map, maplibregl, options?)

  • map - MapLibre map instance
  • maplibregl - MapLibre GL namespace
  • options?.onMarkerClick - Optional marker click handler

attachGoogle(map, google, options?)

  • map - Google Maps instance
  • google - Google Maps namespace
  • options?.onMarkerClick - Optional marker click handler

attachMapbox(map, mapboxgl, options?)

  • map - Mapbox map instance
  • mapboxgl - Mapbox GL namespace
  • options?.onMarkerClick - Optional marker click handler

Search Methods

propertiesSearch.search(options)

Runs a properties search with the specified options.

Options:

  • body - Search body with city, country, filters, etc.
  • beforeApplyProperties? - Callback to modify data before applying
  • smartFiltersClearable? - Whether smart filters can be cleared

Returns: Promise with search results

smartFilterSearch.search(options)

Runs a smart filter search with natural language or predefined filters.

Options:

  • query? - Natural language search query
  • filters? - Array of SmartFilter objects
  • onProcessFilters? - Callback to process filter response

Returns: Promise with search results

boundsSearch.perform()

Performs a search within the current map bounds when state.pendingBounds is set.

Returns: Promise with search results

Legacy API

The old separate hooks (useMapFirstCore, useMapLibreAttachment, etc.) have been consolidated into the single useMapFirst hook. If you're using the old API:

// Old API (deprecated)
const { mapFirst, state } = useMapFirstCore({ ... });
useMapLibreAttachment({ mapFirst, map, maplibregl });
const { search } = usePropertiesSearch(mapFirst);

// New unified API
const {
  instance: mapFirst,
  state,
  attachMapLibre,
  propertiesSearch
} = useMapFirst({ ... });

Migration Benefits:

  • Single hook import instead of multiple
  • All functionality available from one hook
  • Better TypeScript support
  • Cleaner, more maintainable code

License

MIT