Using React
On CodeSandbox
The fastest way to get started experimenting with Mappedin JS and React is to fork the Mappedin React Template on CodeSandbox. This template already contains the useVenue and useMapView hooks.
Mappedin JS 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 typescript
cd mappedin-react-app
yarn add @mappedin/mappedin-js
The Mappedin JS 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
.
Ensure that your options passed to this hook are properly memoized to prevent fetching your venue more than once. See the useMemo documentation on react.dev
import { TGetVenueOptions, Mappedin, getVenue } from '@mappedin/mappedin-js';
import React, { useState, useEffect } from 'react';
export default 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 TMapViewOptions section of the API Reference.
Ensure that your options passed to this hook are properly memoized to prevent re-rendering the MapView to the canvas. See the useMemo documentation on react.dev
import { Mappedin, MapView, showVenue, TMapViewOptions } from '@mappedin/mappedin-js';
import { useCallback, useEffect, useRef, useState } from 'react';
export default function useMapView(venue: Mappedin | undefined, options?: TMapViewOptions) {
// Store the MapView instance in a state variable
const [mapView, setMapView] = useState<MapView | undefined>();
const mapRef = useRef<HTMLDivElement | null>(null);
const isRendering = useRef(false);
// Render the MapView asynchronously
const renderVenue = useCallback(
async (el: HTMLDivElement, venue: Mappedin, options?: TMapViewOptions) => {
if (isRendering.current === true || mapView != null) {
return;
}
isRendering.current = true;
const _mapView = await showVenue(el, venue, options);
setMapView(_mapView);
isRendering.current = false;
},
[isRendering, mapView, setMapView],
);
// Pass this ref to the target div which will render the MapView
const elementRef = useCallback(
(element: HTMLDivElement | null) => {
if (element == null) {
return;
}
mapRef.current = element;
if (mapView == null && venue != null && isRendering.current == false) {
renderVenue(element, venue, options);
}
},
[mapView, venue, renderVenue, options],
);
// Intialize the MapView if the element has been created the and venue loaded afterwards
useEffect(() => {
if (mapView) {
return;
}
if (mapRef.current != null && venue != null) {
renderVenue(mapRef.current, venue, options);
}
}, [venue, mapView, renderVenue, options]);
return { mapView, elementRef };
}
Result
Using these two custom hooks we can attach a MapView instance to an element in our React component.
Ensure that your options passed to
useVenue
anduseMapView
are properly memoized to prevent running them more than once. See the useMemo documentation on react.dev
import { TGetVenueOptions } from '@mappedin/mappedin-js';
import '@mappedin/mappedin-js/lib/mappedin.css';
import { useMemo } from 'react';
import './App.css';
import useMapView from './useMapView';
import useVenue from './useVenue';
export default function App() {
// See Trial API key Terms and Conditions
// https://developer.mappedin.com/docs/demo-keys-and-maps
const options = useMemo<TGetVenueOptions>(
() => ({
venue: 'mappedin-demo-mall',
clientId: '5eab30aa91b055001a68e996',
clientSecret: 'RJyRXKcryCMy4erZqqCbuB1NbR66QTGNXVE0x3Pg6oCIlUR1',
}),
[],
);
const venue = useVenue(options);
const { elementRef, mapView } = useMapView(venue);
return <div id="app" ref={elementRef} />;
}
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. Mappedin JS 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, useMemo } from 'react';
import useVenue from '../hooks/useVenue';
import useMapView from '../hooks/useMapView';
import '@mappedin/mappedin-js/lib/mappedin.css';
export default function Map() {
// See Trial API key Terms and Conditions
// https://developer.mappedin.com/docs/demo-keys-and-maps
const options = useMemo<TGetVenueOptions>(
() => ({
venue: 'mappedin-demo-mall',
clientId: '5eab30aa91b055001a68e996',
clientSecret: 'RJyRXKcryCMy4erZqqCbuB1NbR66QTGNXVE0x3Pg6oCIlUR1',
}),
[],
);
const mapRef = useRef<HTMLDivElement | null>(null);
const venue = useVenue(options);
const mapView = useMapView(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.