On CodeSandbox
The fastest way to get started experimenting with Mappedin Web SDK and React is to fork the Mappedin React Template on CodeSandbox. This template already contains the useVenue and useMapView hooks.
Mappedin Web SDK v5 is available as @mappedin/mappedin-js in NPM.
Local Development
To begin building locally, start by initializing a React app and adding @mappedin/mappedin-js.
npx create-react-app mappedin-react-app --template typescriptcd mappedin-react-appyarn add @mappedin/mappedin-js
The Mappedin Web SDK does not provide React components. You will need to use effects to fetch the data and display it within your app. There are many ways you could achieve this depending on your project setup, but in this guide we'll illustrate writing a few custom React hooks.
useVenue
Create a new hook called useVenue
. This hook will asynchronously fetch the Mappedin data using getVenue
. It takes the venue options as a parameter and returns a Mappedin venue object which we pass to useMapView
.
import { TGetVenueOptions, Mappedin, getVenue } from "@mappedin/mappedin-js";import React, { useState, useEffect } from "react";
export function useVenue(options: TGetVenueOptions) { // Store the venue object in a state variable const [venue, setVenue] = useState<Mappedin | undefined>();
// Fetch data asynchronously whenever options are changed useEffect(() => { let ignore = false; const fetchData = async () => { try { const data = await getVenue(options); // Update state variable after data is fetched if (!ignore) { setVenue(data); } } catch (e) { // Handle error console.log(e); setVenue(undefined); } }; fetchData();
return () => { ignore = true; }; }, [options]);
// Return the venue object return venue;}
useMapView
Create another hook called useMapView
. This hook calls the showVenue
method to render the MapView on an element. It takes an HTMLElement and the venue object returned from useVenue
as required parameters. It returns the MapView instance which you can use to manipulate the map.
You can read about the other options you can pass to useMapView in the TShowVenueOptions section of the API Reference.
import { TShowVenueOptions, Mappedin, MapView, showVenue,} from "@mappedin/mappedin-js";import React, { useState, useEffect } from "react";
export function useMapView( el: HTMLElement | null, venue: Mappedin | undefined, options?: TShowVenueOptions) { // Store the MapView instance in a state variable const [mapView, setMapView] = useState<MapView | undefined>();
// Render the MapView asynchronously useEffect(() => { const renderVenue = async () => { // Do nothing if the map container or venue data are not initialized if (el == null || venue == null) { return; }
// Do nothing if the mapView is already rendered with the current venue data if (mapView != null && mapView.venue.venue.id === venue.venue.id) { return; }
// If the mapView has been rendered with old data, destroy it if (mapView != null) { mapView.destroy(); }
// Try to render the mapView try { const _mapView = await showVenue(el, venue, options); setMapView(_mapView); } catch (e) { // Handle error console.log(e); setMapView(undefined); } }; renderVenue(); }, [el, venue, options, mapView]);
// Return the MapView instance return mapView;}
Result
Using these two custom hooks we can attach a MapView instance to an element in our React component.
// See Trial API key Terms and Conditions// https://developer.mappedin.com/guides/api-keysconst options: TGetVenueOptions = { venue: "mappedin-demo-mall", clientId: "5eab30aa91b055001a68e996", clientSecret: "RJyRXKcryCMy4erZqqCbuB1NbR66QTGNXVE0x3Pg6oCIlUR1",};
export default function App() { const mapRef = useRef<HTMLDivElement | null>(null); const venue = useVenue(options); const mapView = useMapView(mapRef.current, venue);
return <div id="app" ref={mapRef} />;}
You should now see a rendering of the Mappedin Demo Mall in your React app, like the CodeSandbox example below.
Other Useful Hooks
The above hooks should help you get the basic MapView instance up and running, which you can then use to add interactivity or floating labels to your map. Depending on your needs, you may wish to create additional hooks to handle these map interactions as well.
We've provided code snippets for some supplementary hooks that can help to get you started.
useMapClick
useMapClick
subscribes to the E_SDK_EVENT.CLICK event. It will fire the passed onClick function when the MapView detects a click event.
import { E_SDK_EVENT, E_SDK_EVENT_PAYLOAD, MapView} from "@mappedin/mappedin-js";import { useCallback, useEffect } from "react";
export function useMapClick( mapView: MapView | undefined, onClick: (payload: E_SDK_EVENT_PAYLOAD[E_SDK_EVENT.CLICK]) => void) { const handleClick = useCallback( (payload: E_SDK_EVENT_PAYLOAD[E_SDK_EVENT.CLICK]) => { onClick(payload); }, [onClick] );
// Subscribe to E_SDK_EVENT.CLICK useEffect(() => { if (mapView == null) { return; }
mapView.on(E_SDK_EVENT.CLICK, handleClick);
// Cleanup return () => { mapView.off(E_SDK_EVENT.CLICK, handleClick); }; }, [mapView, handleClick]);}
You can read more about click events in the Adding Interactivity guide.
useOfflineSearch
useOfflineSearch
creates or reuses an OfflineSearch
instance and passes it a new query all within one hook. It uses a new hook introduced in React 18, useDeferredValue, to improve performance by ensuring state updates are completed before performing another search.
import { Mappedin, OfflineSearch, TMappedinOfflineSearchResult} from "@mappedin/mappedin-js";import { useDeferredValue, useEffect, useState } from "react";
export function useOfflineSearch(venue: Mappedin | undefined, query: string) { // Store the OfflineSearch instance in a state variable const [searchInstance, setSearchInstance] = useState< OfflineSearch | undefined >(); // Store the most recent results const [results, setResults] = useState<TMappedinOfflineSearchResult[]>([]); // Defer the new search query until state updates are complete const deferredQuery = useDeferredValue(query);
// Create the OfflineSearch instance useEffect(() => { if (venue == null) { setSearchInstance(undefined); return; }
const instance = new OfflineSearch(venue); setSearchInstance(instance); }, [venue]);
// Get search results asynchronously useEffect(() => { if (venue == null || searchInstance == null || deferredQuery === "") { setResults([]); return; }
const generateSearchResults = async () => { const results = await searchInstance.search(deferredQuery); setResults(results); }; generateSearchResults(); }, [deferredQuery, venue, searchInstance]);
// Return the most recent results return results;}
When using the useOfflineSearch hook, be sure to memoize the results to apply the useDeferredValue optimization to your component.
const results = useOfflineSearch(venue, searchQuery);const searchResults = useMemo( () => results .filter((result) => result.type === "MappedinLocation") .map((result) => ( <div id="search-result" key={(result.object as MappedinLocation).name} onClick={() => { setSelectedLocation(result.object as MappedinLocation); setSearchQuery(""); }} > {`${result.object.name}`} </div> )), [results]);
You can read more about Mappedin's built-in search in the Search guide.
Result
With additional hooks in place, we can build a performant interactive map experience in our React app. Try clicking on or searching for a location in the sandbox below, or browse the code to see how we put it all together.
Server-side Rendering (SSR) with Next.js
The above hooks will enable successfully rendering a MapView with React, however, issues may be encountered while building the application using a framework such as Next.js. The Mappedin Web SDK is primarily a client-sided application and doesn't support server-side rendering (SSR). Thankfully, Next.js already has a solution for dynamically importing components without SSR.
Isolate all MapView functions to a component which can be rendered on the client side. In this component, perform all the map initialization, including setting up the map event listeners.
import { TGetVenueOptions, MappedinMap, E_SDK_EVENT,} from "@mappedin/mappedin-js";import React, { useRef, useEffect } from "react";import useVenue from "../hooks/useVenue";import useMapView from "../hooks/useMapView";import "@mappedin/mappedin-js/lib/mappedin.css";
// See Trial API key Terms and Conditions// https://developer.mappedin.com/api-keys/const options: TGetVenueOptions = { venue: "mappedin-demo-mall", clientId: "5eab30aa91b055001a68e996", clientSecret: "RJyRXKcryCMy4erZqqCbuB1NbR66QTGNXVE0x3Pg6oCIlUR1",};
export default function Map() { const mapRef = useRef<HTMLDivElement | null>(null); const venue = useVenue(options); const mapView = useMapView(mapRef.current, venue);
useEffect(() => { if (!mapView) return;
// Initialize labels mapView.FloatingLabels.labelAllLocations();
// Add event listener for map change function handleMapChanged(map: MappedinMap) { console.log(map); } mapView.on(E_SDK_EVENT.MAP_CHANGED, handleMapChanged);
return () => { // Cleanup mapView.FloatingLabels.removeAll(); mapView.off(E_SDK_EVENT.MAP_CHANGED, handleMapChanged); }; }, [mapView]);
return <div id="mappedin-map" ref={mapRef} />;}
With all the client-sided work isolated, use Next.js dynamic to safely import the new component without SSR. Be sure to pass the ssr: false
flag to dynamic.
import dynamic from "next/dynamic";const Map = dynamic(() => import("../components/map"), { ssr: false,});
export default function Home() { return ( <main> <Map /> </main> );}
Using this component structure, next build
should complete successfully without SSR errors.