> For the complete documentation index, see [llms.txt](https://docs.mapbox.com/help/llms.txt)

# Build a store locator with React & Mapbox GL JS

This tutorial will guide you through building a modern store locator application using [Mapbox GL JS](https://docs.mapbox.com/mapbox-gl-js/), **React**, **TypeScript**, [**Vite**](https://vite.dev), and [**TailwindCSS**](https://tailwindcss.com). You'll create an interactive map with custom markers, a sidebar listing, and smooth interactions between map and UI components.

By the end of this tutorial you'll have completed the following:

-   Setting up a React project with TypeScript, Vite & TailwindCSS
-   Integrated Mapbox GL JS with React using refs and useEffect
-   Imported GeoJSON data for the store locations
-   Created custom interactive markers using Reacts `createPortal`
-   Built a sidebar with store listings
-   Implemented interactivity across the map, markers and sidebar

You'll use the locations of [a popular lunch spot](http://sweetgreen.com/locations) in the Washington, D.C. area as example data, but the concepts apply to any business with multiple locations.

See a video of the finished product below or checkout the [finished app here](https://docs.mapbox.com/help/help/demos/building-a-store-locator-react/final/index.html).

Your browser doesn't support HTML5 video. Open [link to the video](https://docs.mapbox.com/help/help/assets/medias/tutorials--building-a-store-locator-react-finished-demo-a402b6d82ebedd5626bf949d38138255.mp4).

> **Related content (tutorial): [Build a store locator in standalone JavaScript, HTML & CSS](https://docs.mapbox.com/help/tutorials/building-a-store-locator)**
> 
> This tutorial uses React, Vite & TailwindCSS to build the application. If you'd like to build a store locator with standalone JavaScript & HTML, follow our **Build a store locator with Mapbox GL JS** tutorial.

## Getting started

Before starting this tutorial, you'll need:

-   **A Mapbox access token**: Find yours on the [Access token page](https://console.mapbox.com/account/access-tokens/) of your Developer Console.
-   **Node.js and npm** installed on your machine ([Download here](https://nodejs.org/))
-   **A code editor** like [Visual Studio Code](https://code.visualstudio.com/)
-   **Familiarity with React** and modern JavaScript/TypeScript concepts
-   **Basic understanding of Mapbox GL JS** (helpful but not required)

## Set up the React project with Vite

We'll start by creating a new React project with TypeScript using [Vite](https://vite.dev).

### Create the project

Open your terminal and run:

```bash
$ npm create vite@latest store-locator-react -- --template react-ts
$ cd store-locator-react
$ npm install
```

This creates a new React project with TypeScript support and installs the basic dependencies.

### Install additional dependencies

Add the required packages for our store locator:

```bash
$ npm install mapbox-gl @types/mapbox-gl
$ npm install -D tailwindcss @tailwindcss/vite autoprefixer
```

-   `mapbox-gl`: The core Mapbox library for interactive maps
-   `@types/mapbox-gl`: TypeScript definitions for Mapbox GL JS
-   `tailwindcss`: For styling our components
-   `@tailwindcss/vite`: Vite plugin for Tailwind CSS

### Configure TailwindCSS

Open the project in your code editor and update your `vite.config.ts` with the following highlighted lines to include Tailwind in the project.

```typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// highlight-start
import tailwindcss from '@tailwindcss/vite'
// highlight-end

export default defineConfig({
  plugins: [
    react(), 
    // highlight-start
    tailwindcss()
    // highlight-end
  ],
})
```

### Test the setup

Start the development server:

```bash
$ npm run dev
```

Your basic React app should now be running and visible in the browser at [http://localhost:5173/](http://localhost:5173/).

![Initial app state](https://docs.mapbox.com/help/assets/ideal-img/tutorials--building-a-store-locator-react--initial-app.05cfd59.480.png)

## Create the basic app structure

Now we'll set up the basic layout and structure for our store locator application to include a map and the store listing in a sidebar.

### Update the main App component

Replace the contents of `src/App.tsx` with the code snippet below

```tsx
import { useRef, useEffect } from 'react'
import mapboxgl from 'mapbox-gl'

import 'mapbox-gl/dist/mapbox-gl.css'
import './App.css'

function App() {
  const mapRef = useRef<mapboxgl.Map | null>(null)
  const mapContainerRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    // Set your Mapbox access token
    mapRef.current = new mapboxgl.Map({
      accessToken: 'YOUR_MAPBOX_ACCESS_TOKEN',
      container: mapContainerRef.current!, 
      center: [-77.03915, 38.90025], // Washington DC
      zoom: 12.5,
      config: {
        basemap: { theme: 'faded'}
      }
    })

    return () => {
      mapRef.current?.remove()
    }
  }, [])

  return (
    <div className="flex absolute top-0 left-0 right-0 bottom-0 h-full w-full">
      {/* Sidebar placeholder */}
      <div className="w-1/4 p-4 bg-sg-light-green">
        <h2 className="text-sg-green text-xl font-bold">
          Stores nearby:
        </h2>
      </div>
      
      {/* Map container */}
      <div className="w-3/4">
        <div className="h-full w-full" ref={mapContainerRef} />
      </div>
    </div>
  )
}

export default App
```

The code above imports the required React hooks and imports **Mapbox GL JS** and its CSS and imports the `src/App.css` file. Then inside the `App` component there are 2 `ref`'s defined one for the map object itself and another for the map container `div`.

Next, inside a `useEffect` the map is instantiated. The `new mapboxgl.Map` has an `accessToken` option — be sure to replace `YOUR_MAPBOX_ACCESS_TOKEN` with your own token. It also has a `container` parameter that references the `mapContainerRef`, the `center` defines coordinates for Washington, D.C., the `zoom` is `12.5`, and Mapbox Standard is configured to use the [faded theme](https://docs.mapbox.com/map-styles/standard/guides/#theming).

Lastly, the App component returns the main elements for scaffolding the layout using TailwindCSS classes. The outer container is a full screen wrapper. There is a 1/4 width sidebar and then a 3/4 width map container with the associated `ref`.

### Import Tailwind styles and clean up CSS

Open `src/index.css` and replace all the CSS styles with the following:

```css
@import "tailwindcss";

@theme {
  --color-sg-green : #00473C;
  --color-sg-light-green : #f4f3e7;
}
```

This imports TailwindCSS classes to the projects CSS file and also adds 2 custom color values that you will use in your classes. These values match the lunch spot's brand colors and can be accessed in our components with classes like `text-sg-green` or `bg-sg-green`.

Next remove all styles from the `src/App.css` file.

At this point, you should see a split-screen layout with an empty sidebar on the left and a Mapbox map of Washington DC on the right which you can interact with by panning and zooming.

![Basic layout of the app with a map loaded](https://docs.mapbox.com/help/assets/ideal-img/tutorials--building-a-store-locator-react--basic-layout.62859c3.480.png)

## Add store data to the app

Now you'll add the store location data to our application. You'll create a file with the lunch spot store locations in the Washington DC area and connect it to the React app.

### Create the store data file

First, create the TypeScript interface and add the data for the stores. Create `src/assets/locations.ts` and paste in the following.

Title: `src/assets/locations.ts`

```javascript
export interface StoreFeature {
  type: "Feature";
  geometry: {
    type: "Point";
    coordinates: [number, number];
  };
  properties: {
    name: string;
    phoneFormatted: string;
    phone: string;
    address: string;
    city: string;
    country: string;
    postalCode: string;
    state: string;
  };
}

export const storeLocations: StoreFeature[] = [
      {
        "type": "Feature",
        "geometry": {
          "type": "Point",
          "coordinates": [-77.049766, 38.900772]
        },
        "properties": {
          "name": "Foggy Bottom",
          "phoneFormatted": "(202) 507-8357",
          "phone": "2025078357",
          "address": "2221 I St NW",
          "city": "Washington DC",
          "country": "United States",
          "postalCode": "20037",
          "state": "D.C."
        }
      },
      {
        "type": "Feature",
        "geometry": {
          "type": "Point",
          "coordinates": [-77.043929, 38.910525]
        },
        "properties": {
          "name": "Dupont",
          "phoneFormatted": "(202) 387-9338",
          "phone": "2023879338",
          "address": "1512 Connecticut Ave NW",
          "city": "Washington DC",
          "country": "United States",
          "postalCode": "20036",
          "state": "D.C."
        }
      },
      {
        "type": "Feature",
        "geometry": {
          "type": "Point",
          "coordinates": [-77.002583742142, 38.887041080933]
        },
        "properties": {
          "name": "Capitol Hill",
          "phoneFormatted": "(202) 547-9338",
          "phone": "2025479338",
          "address": "221 Pennsylvania Ave SE",
          "city": "Washington DC",
          "country": "United States",
          "postalCode": "20003",
          "state": "D.C."
        }
      },      
      {
        "type": "Feature",
        "geometry": {
          "type": "Point",
          "coordinates": [-77.031333, 38.919315]
        },
        "properties": {
          "name": "14th + W",
          "phoneFormatted": "(215) 386-1365",
          "phone": "2025062956",
          "address": "1325 W St NW",
          "city": "Washington DC",
          "country": "United States",
          "postalCode": "20009",
          "state": "D.C."
        }
      },
      {
        "type": "Feature",
        "geometry": {
          "type": "Point",
          "coordinates": [-77.039751, 38.901223 ]
        },
        "properties": {
          "name": "Farragut Square",
          "phoneFormatted": "(202) 506-3079",
          "phone": "2025063079",
          "address": "888 17th St NW",
          "city": "Washington DC",
          "country": "United States",
          "postalCode": "20006",
          "state": "D.C."
        }
      },
      {
        "type": "Feature",
        "geometry": {
          "type": "Point",
          "coordinates": [-77.063121, 38.9037889]
        },
        "properties": {
          "name": "Georgetown",
          "phoneFormatted": "(202) 838-4300",
          "phone": "2028384300",
          "address": "1044 Wisconsin Ave NW",
          "city": "Washington DC",
          "country": "United States",
          "postalCode": "20007",
          "state": "D.C."
        }
      },
    { 
        "type": "Feature",
        "geometry": {
          "type": "Point",
          "coordinates": [-77.033967, 38.909834]
        },
        "properties": {
          "name": "Logan Circle",
          "phoneFormatted": "(202) 234-7336",
          "phone": "2022347336",
          "address": "1461 P St NW",
          "city": "Washington DC",
          "country": "United States",
          "postalCode": "20005",
          "state": "D.C."
        }
      },
    { 
        "type": "Feature",
        "geometry": {
          "type": "Point",
          "coordinates": [-77.020081, 38.901820 ]
        },
        "properties": {
          "name": "Mount Vernon",
          "phoneFormatted": "(202) 793-7300 ",
          "phone": "2027937300 ",
          "address": "601 Massachusetts Avenue Northwest suite 110",
          "city": "Washington DC",
          "country": "United States",
          "postalCode": "20001",
          "state": "D.C."
        }
      },
    { 
        "type": "Feature",
        "geometry": {
          "type": "Point",
          "coordinates": [ -77.000300, 38.874972 ]
        },
        "properties": {
          "name": "Navy Yard",
          "phoneFormatted": "(202) 554-7336 ",
          "phone": "2025547336 ",
          "address": " 1212 4th St SE",
          "city": "Washington DC",
          "country": "United States",
          "postalCode": "20003",
          "state": "D.C."
        }
      },
    { 
        "type": "Feature",
        "geometry": {
          "type": "Point",
          "coordinates": [-77.021372,  38.895985 ]
        },
        "properties": {
          "name": "Penn Quarter",
          "phoneFormatted": "(202) 804-2250 ",
          "phone": "2028042250 ",
          "address": "624 E. Street, NW",
          "city": "Washington DC",
          "country": "United States",
          "postalCode": "20004",
          "state": "D.C."
        }
      },
    { 
        "type": "Feature",
        "geometry": {
          "type": "Point",
          "coordinates": [-76.999066, 38.909368 ]
        },
        "properties": {
          "name": "Union Market",
          "phoneFormatted": "(202) 891-5954   ",
          "phone": "2028915954   ",
          "address": "1304 4th Street Northeast",
          "city": "Washington DC",
          "country": "United States",
          "postalCode": "20002",
          "state": "D.C."
        }
      },
    { 
        "type": "Feature",
        "geometry": {
          "type": "Point",
          "coordinates": [-77.049349, 38.905231 ]
        },
        "properties": {
          "name": "West End",
          "phoneFormatted": "(202) 629-2100   ",
          "phone": "2026292100  ",
          "address": "2238 M St NW",
          "city": "Washington DC",
          "country": "United States",
          "postalCode": "20037",
          "state": "D.C."
        }
      }
    ];
```

The `storeLocations` variable is an array of the `StoreFeature` type, which follows the [GeoJSON specification](https://geojson.org/) for geographic data. Each store is represented as a Feature with:

-   **geometry**: Contains the geographic coordinates (longitude, latitude)
-   **properties**: Contains all the store information like name, address, phone, etc.

### Update App.tsx to use the store data

Now let's update `src/App.tsx` to import and use our store data:

```tsx
// highlight-start
import { useRef, useEffect, useState } from 'react' // add useState hook
// highlight-end
import mapboxgl from 'mapbox-gl'
// highlight-start
import { storeLocations } from './assets/locations'
import type { StoreFeature } from './assets/locations'
// highlight-end

import 'mapbox-gl/dist/mapbox-gl.css';
import './App.css'

function App() {
  const mapRef = useRef<mapboxgl.Map | null>(null)
  const mapContainerRef = useRef<HTMLDivElement>(null)
  // highlight-start
  const [stores] = useState<StoreFeature[]>(storeLocations)
  const [mapLoaded, setMapLoaded] = useState(false)
  // highlight-end

  useEffect(() => {
    mapRef.current = new mapboxgl.Map({
      accessToken: 'YOUR_MAPBOX_ACCESS_TOKEN',
      container: mapContainerRef.current!, 
      center: [-77.03915, 38.90025], // Centered on Washington DC
      zoom: 12.5,
      config: {
        basemap: { theme: 'faded'}
      }
    })

    // highlight-start
    mapRef.current.on('load', () => {
      setMapLoaded(true)
    })
    // highlight-end

    return () => {
      mapRef.current?.remove()
    }
  }, [])

  return (
    <div className="flex absolute top-0 left-0 right-0 bottom-0 h-full w-full">
      {/* Sidebar placeholder */}
      <div className="w-1/4 p-4 bg-sg-light-green">
        <h2 className="text-sg-green text-xl font-bold">
          // highlight-start
          Stores nearby: {stores.length}
          // highlight-end
        </h2>
      </div>
      {/* map container */}
      <div className="w-3/4">
        <div className="h-full w-full" ref={mapContainerRef} />
      </div>
    </div>
  )
}

export default App
```

In the code above you have imported the store locations & `StoreFeature` type and added state to store the locations and to handle a boolean value of `mapLoaded` which you set inside a [`map.on('load')`](https://docs.mapbox.com/mapbox-gl-js/api/map/#map#on) event. In the next step you'll use this to render our markers only after the map loads. Finally you update the sidebar to list the number of stores with `stores.length`. You can verify the data is being properly imported by reloading the app. You should see **Store nearby: 12** being rendered into the sidebar.

![Store number in sidebar](https://docs.mapbox.com/help/assets/ideal-img/tutorials--building-a-store-locator-react--store-number.8d22675.480.png)

Now that you have imported the data & set up the state, next you'll visualize that data on the map with markers.

## Add store markers to the map

Now you'll create markers for each store location and add them to the map.

To start download this zip file, extract the 2 SVG's and place them in the `/public` folder of your project.

[Download Marker SVG's](https://docs.mapbox.com/help/help/img/store-locator-react/markers.zip)

### Create the Marker component

Create a `src/Marker.tsx` file and copy in the following code:

```tsx
import { useEffect, useRef } from "react"
import { createPortal } from "react-dom";
import mapboxgl from 'mapbox-gl'
import type { StoreFeature } from './assets/locations';

interface MarkerProps {
  feature: StoreFeature
  map: mapboxgl.Map
}

const Marker = ({ map, feature }: MarkerProps) => {
    const { geometry } = feature

    const contentRef = useRef(document.createElement("div"));
    const markerRef = useRef<mapboxgl.Marker | null>(null)
    
    useEffect(() => {

        markerRef.current = new mapboxgl.Marker(contentRef.current)
            .setLngLat([geometry.coordinates[0], geometry.coordinates[1]])
            .addTo(map)

        return () => {
             markerRef.current?.remove()
        }
    }, [])

    return (
         <>
            {createPortal(
                <div 
                    className={'bg-contain bg-no-repeat cursor-pointer transition w-[37px] h-[40px]'}
                    style={{
                        backgroundImage: 'url("./sg-marker.svg")',
                    }}>
                </div>,
                contentRef.current
            )}
        </>
    )
}

export default Marker
```

This component creates a new Mapbox [`Marker`](https://docs.mapbox.com/mapbox-gl-js/api/markers/#marker), sets the `Marker` location from the coordinates of the feature geometry and adds it to the map. There are 2 refs used, one for the `Marker` object itself and another to manage the content of the `Marker`. React's `createPortal` is used to render custom marker content directly into the Mapbox `Marker` DOM element. The `Marker` div uses one of the custom marker images and includes Tailwind classes to the set the size of the marker, set background image properties and the cursor to pointer.

### Update App.tsx to use markers

Next, copy the highlighted code sections into `src/App.tsx` to import and render the Marker component. The code below imports the `Marker` component, and if the `mapLoaded` is `true` maps over the `stores` array to return a `Marker` component for each store location.

```tsx
import { useRef, useEffect, useState } from 'react'
import mapboxgl from 'mapbox-gl'
// highlight-start
import Marker from './Marker'
// highlight-end
import { storeLocations } from './assets/locations'
import type { StoreFeature } from './assets/locations'

import 'mapbox-gl/dist/mapbox-gl.css'
import './App.css'

function App() {
  const mapRef = useRef<mapboxgl.Map | null>(null)
  const mapContainerRef = useRef<HTMLDivElement>(null)
  const [stores] = useState<StoreFeature[]>(storeLocations)
  const [mapLoaded, setMapLoaded] = useState(false)

  useEffect(() => {
    mapRef.current = new mapboxgl.Map({
      accessToken: 'YOUR_MAPBOX_ACCESS_TOKEN',
      container: mapContainerRef.current!, 
      center: [-77.03915, 38.90025],
      zoom: 12.5,
      style: 'mapbox://styles/mapbox/light-v11'
    })

    mapRef.current.on('load', () => {
      setMapLoaded(true)
    })

    return () => {
      mapRef.current?.remove()
    }
  }, [])

  return (
    <div className="flex absolute top-0 left-0 right-0 bottom-0 h-full w-full">
      {/* Sidebar placeholder */}
      <div className="w-1/4 p-4 bg-sg-light-green">
        <h2 className="text-sg-green text-xl font-bold">
          Stores nearby: {stores.length}
        </h2>
      </div>
      
      {/* Map container */}
      <div className="w-3/4">
        <div className="h-full w-full" ref={mapContainerRef} />
        // highlight-start
        {mapLoaded && stores.map(location => (
          <Marker 
            key={location.properties.name}
            feature={location} 
            map={mapRef.current!}
          />
        ))}
        // highlight-end
      </div>
    </div>
  )
}

export default App
```

Now when you reload your app you should see the store locations rendered on the map.

![Map with Markers and empty sidebar](https://docs.mapbox.com/help/assets/ideal-img/tutorials--building-a-store-locator-react--markers.24969dd.480.png)

Next, you'll build a listing of store locations in the sidebar.

## Build the sidebar store listing

Now create a sidebar that displays all store locations with detailed information.

### Create the Sidebar component

Create `src/Sidebar.tsx` and add the following content:

```tsx
import type { StoreFeature } from './assets/locations'

interface SidebarProps {
  stores: StoreFeature[]
}

const Sidebar = ({ stores  }: SidebarProps) => {

  return (
    <div className="w-1/4 p-4 overflow-y-auto bg-sg-light-green shadow-xl z-10">
      <h2 className="text-sg-green text-xl font-bold mb-4">
        Stores nearby: {stores.length}
      </h2>
      
      {stores.map((store) => {
        return (
          <div 
            key={store.properties.name} 
            className={'bg-transparent hover:bg-white/50 relative flex flex-col my-4 border border-sg-green rounded-lg transition-all duration-200 cursor-pointer p-4'}>
               
            <h4 className="mb-2 text-sg-green text-xl font-semibold">{store.properties.name}</h4>
            <div className="text-sg-green leading-normal font-light">
                <div>
                    <span className="font-bold text-sm">Address: </span>{store.properties.address}
                </div>    
                <div>
                    <span className="font-bold text-sm">Phone: </span>{store.properties.phoneFormatted}
                </div>
            </div>
          </div>
        )
      })}
    </div>
  )
}

export default Sidebar
```

### Update App.tsx to use the Sidebar

Now update `src/App.tsx` to import and use the Sidebar component. You'll replace the previous sidebar placeholder HTML with the new `<Sidebar>` component.

```tsx
import { useRef, useEffect, useState } from 'react'
import mapboxgl from 'mapbox-gl'
import Marker from './Marker'
// highlight-start
import Sidebar from './Sidebar'
// highlight-end
import { storeLocations } from './assets/locations'
import type { StoreFeature } from './assets/locations'

import 'mapbox-gl/dist/mapbox-gl.css'
import './App.css'

function App() {
  const mapRef = useRef<mapboxgl.Map | null>(null)
  const mapContainerRef = useRef<HTMLDivElement>(null)
  const [stores] = useState<StoreFeature[]>(storeLocations)
  const [mapLoaded, setMapLoaded] = useState(false)

  useEffect(() => {
    mapRef.current = new mapboxgl.Map({
      accessToken: 'YOUR_MAPBOX_ACCESS_TOKEN',
      container: mapContainerRef.current!, 
      center: [-77.03915, 38.90025],
      zoom: 12.5,
      style: 'mapbox://styles/mapbox/light-v11'
    })

    mapRef.current.on('load', () => {
      setMapLoaded(true)
    })

    return () => {
      mapRef.current?.remove()
    }
  }, [])

  return (
    <div className="flex absolute top-0 left-0 right-0 bottom-0 h-full w-full">
      // highlight-start
      // Remove previous sidebar placeholder and replace with this code
     <Sidebar stores={stores}/>
     // highlight-end
      <div className="w-3/4">
        <div className="h-full w-full" ref={mapContainerRef} />
        {mapLoaded && stores.map(location => (
          <Marker 
            key={location.properties.name}
            feature={location} 
            map={mapRef.current!}
          />
        ))}
      </div>
    </div>
  )
}

export default App
```

Now when you reload your app, you should see the sidebar listing on the left inside a scrollable container.

![App with markers and sidebar listing](https://docs.mapbox.com/help/assets/ideal-img/tutorials--building-a-store-locator-react--sidebar-listing.040f544.480.png)

Now that you have the store locations rendering on the map and in the sidebar, in the next step you'll add interactivity to map markers and the sidebar listing to show a selected store across both the map and sidebar.

## Add interactivity - Markers & Map

For this step you'll create a `selectedStore` state and handle interactions with the Marker and create a side effect in the map.

### Add `selectedStore` state and event handler to `App.tsx`

Update `src/App.tsx` with the following highlighted code snippets:

```tsx
import { useRef, useEffect, useState } from 'react'
import mapboxgl from 'mapbox-gl'
import Sidebar from './Sidebar'
import Marker from './Marker'
import { storeLocations } from './assets/locations'
import type { StoreFeature } from './assets/locations'

import 'mapbox-gl/dist/mapbox-gl.css';
import './App.css'

function App() {

  const mapRef = useRef<mapboxgl.Map | null>(null)
  const mapContainerRef = useRef<HTMLDivElement>(null)
  const [stores] = useState<StoreFeature[]>(storeLocations)
  const [mapLoaded, setMapLoaded] = useState(false)
  // highlight-start
  const [selectedStore, setSelectedStore] = useState<StoreFeature | null>(null)
  // highlight-end

  useEffect(() => {

    mapRef.current = new mapboxgl.Map({
      accessToken: 'YOUR_MAPBOX_ACCESS_TOKEN',
      container: mapContainerRef.current!, 
      center: [-77.03915, 38.90025],
      zoom: 12.5,
      config: {
        basemap: {
          theme: "faded"
        }
      },
    });

    mapRef.current.on('load', () => {
      setMapLoaded(true)
    })

    return () => {
      mapRef.current?.remove()
    }
  },[])

  // highlight-start
  useEffect(() => {
    if (!selectedStore ) return

    mapRef.current!.flyTo({center: [selectedStore.geometry.coordinates[0], selectedStore.geometry.coordinates[1]], zoom: 13, duration: 1000})

  },[selectedStore])
  // highlight-end

  return (
    <div className="flex absolute top left right bottom h-full w-full">
      <Sidebar stores={stores} />
      <div className="w-3/4">
        <div className="h-full w-full" ref={mapContainerRef}/>
        {mapLoaded && mapRef.current && stores.map(location => (
          <Marker 
            key={location.properties.name}
            feature={location} 
            map={mapRef.current!}
            // highlight-start
            setSelectedStore={setSelectedStore}
            selectedStore={selectedStore}/>
            // highlight-end
        ))}
      </div>
    </div>
  )
}

export default App
```

In the updates above, you declare a new state variable `selectedStore` with a type of `<StoreFeature | null>`. Then a `useEffect()` is added to handle moving the map with a [`flyTo`](https://docs.mapbox.com/mapbox-gl-js/api/map/#map#flyto) event using the coordinates of the selected feature. Lastly you pass in these new props to the `<Marker>` component.

### Add new props and `onClick` handler to `Marker.tsx`

Next you are going to update your `<Marker>` component to consume the new props and manage click events. When a Marker is clicked it will call `setSelectedStore` which will set the `selectedStore` state variable and pass that back up to the main `App.tsx`. The Marker also consumes this state and if the Marker's feature matches the `selectedStore` it will render a different icon.

Open `src/Marker.tsx` and update it with the following highlighted code snippets:

```tsx
import { useEffect, useRef } from "react"
import { createPortal } from "react-dom";
import mapboxgl from 'mapbox-gl'
import type { StoreFeature } from './assets/locations';

interface MarkerProps {
  feature: StoreFeature
  map: mapboxgl.Map
  // highlight-start
  selectedStore: StoreFeature | null
  setSelectedStore: Function
  // highlight-end
}
// highlight-start
const Marker = ({ map, feature, selectedStore, setSelectedStore }: MarkerProps) => {
// highlight-end
    const { geometry } = feature

    const contentRef = useRef(document.createElement("div"));
    const markerRef = useRef<mapboxgl.Marker | null>(null)
    // highlight-start
    const isSelected = feature.properties.name === selectedStore?.properties.name;
    // highlight-end
    
    useEffect(() => {

        markerRef.current = new mapboxgl.Marker(contentRef.current)
            .setLngLat([geometry.coordinates[0], geometry.coordinates[1]])
            .addTo(map)

        return () => {
             markerRef.current?.remove()
        }
    }, [])

    return (
         <>
            {createPortal(
                <div 
                // highlight-start
                onClick={() => setSelectedStore(feature)}
                // highlight-end
                    className={'bg-contain bg-no-repeat cursor-pointer transition w-[37px] h-[40px]'}
                    // highlight-start
                     style={{
                        backgroundImage: (
                            isSelected
                            ? 'url("./sg-marker-selected.svg")' 
                            : 'url("./sg-marker.svg")'),
                    }}>
                    // highlight-end
                </div>,
                contentRef.current
            )}
        </>
    )
}

export default Marker
```

Reload the app, and click a marker on the map. The map should move to center on the marker and the clicked marker should switch to the 'selected' marker icon of a different color.

Your browser doesn't support HTML5 video. Open [link to the video](https://docs.mapbox.com/help/help/assets/medias/tutorials--building-a-store-locator-react--marker-interactivity-2ccdc61f24d7a09c8d389975eeb76a33.mp4).

## Add interactivity - Sidebar

Now you have covered one direction of interactivity, from map to app. Next you'll update the `Sidebar.tsx` component to consume the `selectedStore` state and allow the Sidebar to set the selected Store (affecting the map) as well as adding a side effect to scroll the selected store listing into view within the sidebar. This will complete interactivity from the opposite direction of app to the map.

### Update `App.tsx`

First update `App.tsx` to pass in the state variables to the `<Sidebar>` component.

```tsx
...
return (
    <div className="flex absolute top-0 left-0 right-0 bottom-0 h-full w-full">
      <Sidebar 
        stores={stores}
        // highlight-start
        selectedStore={selectedStore}
        setSelectedStore={setSelectedStore}/>
        // highlight-end
      
      {/* Map container */}
      <div className="w-3/4">
        <div className="h-full w-full" ref={mapContainerRef} />
...
```

### Update `Sidebar.tsx`

Now update your `Sidebar.tsx` file with the following highlighted code snippets:

```tsx
// highlight-start
import { useEffect, useRef } from 'react'
// highlight-end
import type { StoreFeature } from './assets/locations'

interface SidebarProps {
  stores: StoreFeature[]
  // highlight-start
  selectedStore: StoreFeature | null
  setSelectedStore: Function
  // highlight-end
}
// highlight-start
const Sidebar = ({ stores, selectedStore, setSelectedStore }: SidebarProps) => {

    const storeRefs = useRef<{ [key: string]: HTMLDivElement | null }>({})

    // Scroll to the active location when it changes (desktop only)
    useEffect(() => {
        if (
        selectedStore &&
        storeRefs.current &&
        storeRefs.current[selectedStore.properties.name]
        ) {
        const element = storeRefs.current[selectedStore.properties.name];
        if (element) {
            element.scrollIntoView({
            behavior: 'smooth', // Optionally smooth scrolling
            block: 'start' // Align the element to the top of the container
            });
        }
        }
    }, [selectedStore])
  // highlight-end

  return (
    <div className="w-1/4 p-4 overflow-y-auto bg-sg-light-green shadow-xl z-10">
      <h2 className="text-sg-green text-xl font-bold mb-4">
        Stores nearby: {stores.length}
      </h2>
      
      {stores.map((store) => {
        // highlight-start
        const isSelected = store.properties.name === selectedStore?.properties.name;
        // highlight-end
        return (
          <div 
            key={store.properties.name} 
            // highlight-start
            ref={(el) => {storeRefs.current[store.properties.name] = el}}
            onClick={()=> setSelectedStore(store)}
            className={`${isSelected ? 'bg-white' : 'bg-transparent'} hover:bg-white/50 relative flex flex-col my-4 border border-sg-green rounded-lg transition-all duration-200 cursor-pointer p-4`}>
            // highlight-end    
            <h4 className="mb-2 text-sg-green text-xl font-semibold">{store.properties.name}</h4>
            <div className="text-sg-green leading-normal font-light">
                <div>
                    <span className="font-bold text-sm">Address: </span>{store.properties.address}
                </div>    
                <div>
                    <span className="font-bold text-sm">Phone: </span>{store.properties.phoneFormatted}
                </div>
            </div>
          </div>
        )
      })}
    </div>
  )
}

export default Sidebar
```

The additional code above imports React's `useEffect` and `useRef` and updates the interface and props for the component. It declares a `storeRefs` variable that tracks all the DIV elements for the store listing. The `storeRefs` variable is populated by the `ref` prop of the sidebar listing DIV.

```js
...
 <div 
    key={store.properties.name} 
    // highlight-start
    ref={(el) => {storeRefs.current[store.properties.name] = el}}
    // highlight-end
    onClick={()=> setSelectedStore(store)}
...
```

This line adds the name of the store and the corresponding DOM reference in the sidebar listing as a key value pair to `storeRefs`.

Next the `useEffect()` checks for a `selectedStore`, looks for a match in the `storeRefs`, if it exists it uses the [`scrollIntoView`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView) API to scroll the selected store's DIV into view in the sidebar container. Lastly you added an `onClick` handler to set the selected store and set a selected class via Tailwind classes.

When you reload the app, you should see interaction and highlighted states connected in both the sidebar and the map.

Your browser doesn't support HTML5 video. Open [link to the video](https://docs.mapbox.com/help/help/assets/medias/tutorials--building-a-store-locator-react--final-product-421aaeee8e6aef9c330a3991a5dd3c32.mp4).

## Final product

**Congratulations!** 🎉 You've successfully built a modern, interactive store locator application using React, TypeScript, Vite, TailwindCSS and Mapbox GL JS.

[See a full screen demo of the final app.](https://docs.mapbox.com/help/help/demos/building-a-store-locator-react/final/index.html)

### What we covered

-   Setting up a React project with TypeScript, Vite & TailwindCSS
-   Integrated Mapbox GL JS with React using refs and useEffect
-   Imported GeoJSON data for the store locations
-   Created custom interactive markers using Reacts `createPortal`
-   Built a sidebar with store listings
-   Implemented interactivity across the map, markers and sidebar

### Next steps

If you'd like to continue improving this app, here are some ideas to try out.

-   **Responsive Design**: The main layout of this app doesn't work well on small screens. Instead of listing locations in the sidebar, you could hide the sidebar on small screens and leverage [`Popup`](https://docs.mapbox.com/mapbox-gl-js/api/markers/#popup)s to display store information in the map UI when a marker is clicked, or build your own panel at the bottom of the screen to show a selected stores information.
-   **Sort stores by distance**: Get the users location with the [GeoLocation](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API/Using_the_Geolocation_API) browser API and then sort the stores listing by distance using [Turf.js](https://turfjs.org/)

### Learn more

If you want to explore a more full featured store locator, checkout our [Store Locator Demo App](https://labs.mapbox.com/demo-store-locator) and browse the source code in the [public-tools-and-demos](https://github.com/mapbox/public-tools-and-demos/tree/main/projects/demo-store-locator) repository.

Or see our other tutorials for using [Mapbox products with React](https://docs.mapbox.com/help/tutorials/?search=react).