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

# Add Point of Interest (POI) Search to a Map in a React app

This Mapbox tutorial walks through how to integrate Points of Interest (POI) search results from the [Mapbox Search Box API](https://docs.mapbox.com/api/search/search-box/) into a React app, allowing the user to discover nearby businesses and services for a given map location.

You'll learn how to:

-   Build UI to trigger category search queries in a mapping application.
-   Use the `SearchBoxCore` component of [Mapbox Search JS](https://docs.mapbox.com/mapbox-search-js/api/core/search/#searchboxcore) to search for POIs.
-   Get the map's bounds and use them as a search option to limit results to the current map view.
-   Use a React component to add custom markers and popups to the map for each point of interest in the search results.
-   Add a "search this area" button to trigger another category search if the user moves the map.

Buttons on a map to search for nearby points of interest is a common UX pattern in mapping applications and can be useful for users to find nearby businesses, services, or other locations of interest. For example, in a real estate app, users may want to explore nearby schools, parks, or grocery stores. In a travel app, users may want to find nearby restaurants, hotels, or attractions.

The Mapbox [Search Box API](https://docs.mapbox.com/api/search/search-box/) provides a powerful and flexible way to search for points of interest, and the `SearchBoxCore` component of *Mapbox Search JS* allows for seamless integration in a JavaScript environment.

The demo below shows the finished product of this tutorial.

-   When the user clicks a button, the app sends a request to the Search Box API's category search endpoint, which returns a list of nearby points of interest for the chosen category.
-   The map displays the results using custom markers. Clicking a marker opens a popup with more details about the selected POI.
-   If a category is already selected and the user moves the map, a "Search this area" button appears. Clicking this button triggers a new search for the current map bounds using the selected category.

**Click one of the category buttons to try it out and find nearby points of interest.**

If you would like to run the finished product locally before following the tutorial, you can find the source code for this tutorial on [GitHub](https://github.com/mapbox/tutorials/tree/main/poi-search-react/).

Get ready to start building! This tutorial is designed to be completed in about 25 minutes.

## Prerequisites

Here are a few resources you'll need throughout this tutorial:

-   **Familiarity with front-end development.** Beginner experience with CSS, HTML and JavaScript.
-   **Mapbox account**: [Sign up](https://account.mapbox.com/auth/signup/) for free or [log in](https://console.mapbox.com/) if you already have an account.
-   **A Code editor**: A program like [Visual Studio Code](https://code.visualstudio.com/).

This tutorial assumes you already know the basics of installing Mapbox GL JS in a React app and are familiar with building React components and state management.

If you're new to Mapbox GL JS, familiarize yourself with the [Mapbox GL JS documentation](https://docs.mapbox.com/mapbox-gl-js/) and explore the [Use Mapbox GL JS with React](https://docs.mapbox.com/help/ja/tutorials/use-mapbox-gl-js-with-react/) tutorial.

## Set up a React app, adding a map and category buttons

To start, set up a new React app with Vite. If you need instructions for how to do this, see steps 3 and 4 of the [Use Mapbox GL JS with React](https://docs.mapbox.com/help/ja/tutorials/use-mapbox-gl-js-with-react/) tutorial.

With your development server running, you can overwrite the default `App.jsx` and `App.css` files with the code below.

The following `App` component renders a Mapbox GL JS map and in a React app. It also renders category search buttons which are positioned with CSS to overlay the top left corner of the map. Include it in your React app and be sure to add CSS as needed to give the map container a height and width.

Note that the following code adds a state variable `searchCategory` to track the selected category. This variable is updated when a button is clicked and is used to add an active style to the selected button, but the actual search functionality will be added in the next step.

Since you will be using both Mapbox GL JS and Mapbox Search JS, storing the access token in a constant is a good practice.

The category buttons are defined with a configuration object. It is important to note that the `value` property in this object corresponds to a valid canonical category id that can be used in the Search Box API. A full list of canonical category ids is available via a call to the [Search Box API's category list endpoint](https://docs.mapbox.com/api/search/search-box/#get-category-list).

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

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

const MAPBOX_ACCESS_TOKEN = "YOUR_MAPBOX_ACCESS_TOKEN" 

const DEFAULT_MAP_BOUNDS = [
  [-74.03189, 40.69684],
  [-73.98121, 40.72286]
]

function App() {
  const mapRef = useRef() // ref for the Map() instance
  const mapContainerRef = useRef() // ref for the map container DOM element

  const [searchCategory, setSearchCategory] = useState() // the selected category

  useEffect(() => {
    mapRef.current = new mapboxgl.Map({
      accessToken: MAPBOX_ACCESS_TOKEN, // set the Mapbox access token
      container: mapContainerRef.current, // display the map in this DOM element
      bounds: DEFAULT_MAP_BOUNDS, // set the initial map bounds
      minZoom: 13, // set the minimum zoom level to avoid zooming out too far
      config: {
        basemap: {
          showPointOfInterestLabels: false, // disable POI labels
        }
      },
    })

    // cleanup function: remove the map when the component unmounts
    return () => {
      mapRef.current.remove()
    }
  }, [])

  // configuration array for category search buttons
  const categoryButtons = [
    { label: "☕ Coffee", value: "coffee" },
    { label: "🍽️ Restaurants", value: "restaurant" },
    { label: "🍸 Bars", value: "bar" },
    { label: "🏨 Hotels", value: "hotel" },
    { label: "🏛️ Museums", value: "museum" }
  ]

  return (
    <>
      {/* Show category search buttons */}
      <div className="button-container">
        {categoryButtons.map(({ label, value }) => (
          <button
            key={value}
            onClick={() => setSearchCategory(value)}
            className={`category-button ${searchCategory === value && 'active'}`}
          >
            {label}
          </button>
        ))}
      </div>

      {/* Map container */}
      <div id='map-container' ref={mapContainerRef} />
    </>
  )
}

export default App

```

The following CSS styles the map and the buttons. The buttons are positioned in a fixed container in the top left corner of the map, with some margin to separate them from the edge of the map.

```css
html, body, #root, #map-container {
  width: 100%;
  height: 100%;
  margin: 0;
}

.button-container {
  display: flex; 
  position: fixed; 
  left: 0.75rem; 
  z-index: 50; 
  margin-top: 0.75rem; 
  margin-top: 0.875rem; 
  flex-direction: column; 
  align-items: flex-start; 
}

button {
  color: #1F2937;
  background-color: #ffffff;
  border-radius: 9999px;
  padding: 0.5rem 1rem;
  margin-bottom: 0.5rem;
  font-size: 0.875rem;
  line-height: 1.25rem;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
  transition: all 0.2s ease-in-out;
  border: 1px solid #e5e7eb;
  cursor: pointer;
}

button:hover {
  background-color: #f3f4f6;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
}

button.active, button:active {
  background-color: #e5e7eb;
  box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.06);
}
```

Be sure to install the Mapbox GL JS package in your React app if you haven't already.

```bash
$ npm install mapbox-gl
```

Run the development server and open your browser to see the map. You should see a map of lower Manhattan, New York City, with buttons to search for nearby points of interest. When you click a button, you will see that it receives an `active` class and the button's background color changes.

```bash
$ npm run dev
```

![The initial map view](https://docs.mapbox.com/help/ja/assets/ideal-img/tutorials--poi-search-react--3.e1795aa.480.png)

With the main `Map` component rendering a Mapbox GL map and category buttons, you are ready to fetch POI data using **Mapbox Search JS**.

## Fetch POI data with Mapbox Search JS

The next step is to fetch POI data when the user clicks a category button. To do this, you will use the `SearchBoxCore` component of Mapbox Search JS. To get the correct results, you will pass the selected category and the map's current bounds to the `SearchBoxCore` component.

First, install Mapbox Search JS Core in your React app.

```bash
$ npm install @mapbox/search-js-core
```

Next, make the following updates to the `App` component:

1.  Import the `SearchBoxCore` and `SessionToken` component from Mapbox Search JS.

```jsx
import { useState, useEffect, useRef } from 'react'
import mapboxgl from 'mapbox-gl'
import { SearchBoxCore, SessionToken } from '@mapbox/search-js-core'


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

const MAPBOX_ACCESS_TOKEN = "YOUR_MAPBOX_ACCESS_TOKEN" // replace

const DEFAULT_MAP_BOUNDS = [
  [-74.03189, 40.69684],
  [-73.98121, 40.72286]
]

function App() {
  const mapRef = useRef() // ref for the Map() instance
  const mapContainerRef = useRef() // ref for the map container DOM element
  const searchRef = useRef() // ref for the SearchBoxCore() instance

  const [searchCategory, setSearchCategory] = useState() // the selected category
  const [searchResults, setSearchResults] = useState([]) // an array of search results
  const [mapBounds, setMapBounds] = useState() // the current map bounds
  const [searchBounds, setSearchBounds] = useState() // the bounds of the search results

  // function to perform a category search using the SearchBoxCore() instance
  // uses the current map bounds and the selected category to search for points of interest
  const performCategorySearch = async () => {
    if (!searchCategory || !mapBounds) return;
    const { features } = await searchRef.current.category(searchCategory, { bbox: mapBounds, limit: 25 });
    setSearchResults(features);
    setSearchBounds(mapBounds);

    console.log('Search results:', features)
  };


  useEffect(() => {
    mapRef.current = new mapboxgl.Map({
      accessToken: MAPBOX_ACCESS_TOKEN, // set the Mapbox access token
      container: mapContainerRef.current, // display the map in this DOM element
      bounds: DEFAULT_MAP_BOUNDS, // set the initial map bounds
      minZoom: 13, // set the minimum zoom level to avoid zooming out too far
      config: {
        basemap: {
          showPointOfInterestLabels: false, // disable POI labels
        }
      },
    })

    // when the map is loaded, set mapBounds to the current map bounds
    mapRef.current.on('load', () => {
      setMapBounds(mapRef.current.getBounds().toArray())
    })

    // when the map moves, set mapBounds to the current map bounds
    mapRef.current.on('moveend', () => {
      setMapBounds(mapRef.current.getBounds().toArray())
    })

     // instantiate the search box
        searchRef.current = new SearchBoxCore({ accessToken: MAPBOX_ACCESS_TOKEN })
        new SessionToken();

    // cleanup function: remove the map when the component unmounts
    return () => {
      mapRef.current.remove()
    }
  }, [])

    // when the searchCategory changes, perform a category search
    useEffect(() => {
      performCategorySearch()
    }, [searchCategory])

  // configuration array for category search buttons
  const categoryButtons = [
    { label: "☕ Coffee", value: "coffee" },
    { label: "🍽️ Restaurants", value: "restaurant" },
    { label: "🍸 Bars", value: "bar" },
    { label: "🏨 Hotels", value: "hotel" },
    { label: "🏛️ Museums", value: "museum" }
  ]

  return (
    <>
      {/* Show category search buttons */}
      <div className="button-container">
        {categoryButtons.map(({ label, value }) => (
          <button
            key={value}
            onClick={() => setSearchCategory(value)}
            className={`category-button ${searchCategory === value && 'active'}`}
          >
            {label}
          </button>
        ))}
      </div>

      {/* Map container */}
      <div id='map-container' ref={mapContainerRef} />
    </>
  )
}

export default App
```

2.  Add a new `Ref` to store a `SearchBoxCore` instance, and new state variables to store the search results, the map bounds, and the bounds used to fetch the data.

```jsx
import { useState, useEffect, useRef } from 'react'
import mapboxgl from 'mapbox-gl'
import { SearchBoxCore, SessionToken } from '@mapbox/search-js-core'


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

const MAPBOX_ACCESS_TOKEN = "YOUR_MAPBOX_ACCESS_TOKEN" // replace

const DEFAULT_MAP_BOUNDS = [
  [-74.03189, 40.69684],
  [-73.98121, 40.72286]
]

function App() {
  const mapRef = useRef() // ref for the Map() instance
  const mapContainerRef = useRef() // ref for the map container DOM element
  const searchRef = useRef() // ref for the SearchBoxCore() instance

  const [searchCategory, setSearchCategory] = useState() // the selected category
  const [searchResults, setSearchResults] = useState([]) // an array of search results
  const [mapBounds, setMapBounds] = useState() // the current map bounds
  const [searchBounds, setSearchBounds] = useState() // the bounds of the search results

  // function to perform a category search using the SearchBoxCore() instance
  // uses the current map bounds and the selected category to search for points of interest
  const performCategorySearch = async () => {
    if (!searchCategory || !mapBounds) return;
    const { features } = await searchRef.current.category(searchCategory, { bbox: mapBounds, limit: 25 });
    setSearchResults(features);
    setSearchBounds(mapBounds);

    console.log('Search results:', features)
  };


  useEffect(() => {
    mapRef.current = new mapboxgl.Map({
      accessToken: MAPBOX_ACCESS_TOKEN, // set the Mapbox access token
      container: mapContainerRef.current, // display the map in this DOM element
      bounds: DEFAULT_MAP_BOUNDS, // set the initial map bounds
      minZoom: 13, // set the minimum zoom level to avoid zooming out too far
      config: {
        basemap: {
          showPointOfInterestLabels: false, // disable POI labels
        }
      },
    })

    // when the map is loaded, set mapBounds to the current map bounds
    mapRef.current.on('load', () => {
      setMapBounds(mapRef.current.getBounds().toArray())
    })

    // when the map moves, set mapBounds to the current map bounds
    mapRef.current.on('moveend', () => {
      setMapBounds(mapRef.current.getBounds().toArray())
    })

     // instantiate the search box
        searchRef.current = new SearchBoxCore({ accessToken: MAPBOX_ACCESS_TOKEN })
        new SessionToken();

    // cleanup function: remove the map when the component unmounts
    return () => {
      mapRef.current.remove()
    }
  }, [])

    // when the searchCategory changes, perform a category search
    useEffect(() => {
      performCategorySearch()
    }, [searchCategory])

  // configuration array for category search buttons
  const categoryButtons = [
    { label: "☕ Coffee", value: "coffee" },
    { label: "🍽️ Restaurants", value: "restaurant" },
    { label: "🍸 Bars", value: "bar" },
    { label: "🏨 Hotels", value: "hotel" },
    { label: "🏛️ Museums", value: "museum" }
  ]

  return (
    <>
      {/* Show category search buttons */}
      <div className="button-container">
        {categoryButtons.map(({ label, value }) => (
          <button
            key={value}
            onClick={() => setSearchCategory(value)}
            className={`category-button ${searchCategory === value && 'active'}`}
          >
            {label}
          </button>
        ))}
      </div>

      {/* Map container */}
      <div id='map-container' ref={mapContainerRef} />
    </>
  )
}

export default App
```

3.  Initialize `SearchBoxCore` and add listeners to update the map bounds.

In the same `useEffect` hook where you create the map, add two map event listeners that get the current map bounds and update the `searchBounds` state variable. You will want to listen for the `moveend` event, which is triggered when the map stops moving, and the `load` event, which is triggered when the map is first loaded.

In both cases, you will call the `getBounds` method of the `map` instance to get the current map bounds and update the `mapBounds` state variable.

Continuing in the same `useEffect`, call `new SearchBoxCore({ accessToken: MAPBOX_ACCESS_TOKEN })` to create a new instance of `SearchBoxCore`, assigning it to `searchRef` you created above. This instance will call the Search Box API when the user clicks a category button. You must also call `SessionToken()` to create a new session token for the search. This token is used to track the search session and is required for the `SearchBoxCore` instance.

```jsx
import { useState, useEffect, useRef } from 'react'
import mapboxgl from 'mapbox-gl'
import { SearchBoxCore, SessionToken } from '@mapbox/search-js-core'


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

const MAPBOX_ACCESS_TOKEN = "YOUR_MAPBOX_ACCESS_TOKEN" // replace

const DEFAULT_MAP_BOUNDS = [
  [-74.03189, 40.69684],
  [-73.98121, 40.72286]
]

function App() {
  const mapRef = useRef() // ref for the Map() instance
  const mapContainerRef = useRef() // ref for the map container DOM element
  const searchRef = useRef() // ref for the SearchBoxCore() instance

  const [searchCategory, setSearchCategory] = useState() // the selected category
  const [searchResults, setSearchResults] = useState([]) // an array of search results
  const [mapBounds, setMapBounds] = useState() // the current map bounds
  const [searchBounds, setSearchBounds] = useState() // the bounds of the search results

  // function to perform a category search using the SearchBoxCore() instance
  // uses the current map bounds and the selected category to search for points of interest
  const performCategorySearch = async () => {
    if (!searchCategory || !mapBounds) return;
    const { features } = await searchRef.current.category(searchCategory, { bbox: mapBounds, limit: 25 });
    setSearchResults(features);
    setSearchBounds(mapBounds);

    console.log('Search results:', features)
  };


  useEffect(() => {
    mapRef.current = new mapboxgl.Map({
      accessToken: MAPBOX_ACCESS_TOKEN, // set the Mapbox access token
      container: mapContainerRef.current, // display the map in this DOM element
      bounds: DEFAULT_MAP_BOUNDS, // set the initial map bounds
      minZoom: 13, // set the minimum zoom level to avoid zooming out too far
      config: {
        basemap: {
          showPointOfInterestLabels: false, // disable POI labels
        }
      },
    })

    // when the map is loaded, set mapBounds to the current map bounds
    mapRef.current.on('load', () => {
      setMapBounds(mapRef.current.getBounds().toArray())
    })

    // when the map moves, set mapBounds to the current map bounds
    mapRef.current.on('moveend', () => {
      setMapBounds(mapRef.current.getBounds().toArray())
    })

     // instantiate the search box
        searchRef.current = new SearchBoxCore({ accessToken: MAPBOX_ACCESS_TOKEN })
        new SessionToken();

    // cleanup function: remove the map when the component unmounts
    return () => {
      mapRef.current.remove()
    }
  }, [])

    // when the searchCategory changes, perform a category search
    useEffect(() => {
      performCategorySearch()
    }, [searchCategory])

  // configuration array for category search buttons
  const categoryButtons = [
    { label: "☕ Coffee", value: "coffee" },
    { label: "🍽️ Restaurants", value: "restaurant" },
    { label: "🍸 Bars", value: "bar" },
    { label: "🏨 Hotels", value: "hotel" },
    { label: "🏛️ Museums", value: "museum" }
  ]

  return (
    <>
      {/* Show category search buttons */}
      <div className="button-container">
        {categoryButtons.map(({ label, value }) => (
          <button
            key={value}
            onClick={() => setSearchCategory(value)}
            className={`category-button ${searchCategory === value && 'active'}`}
          >
            {label}
          </button>
        ))}
      </div>

      {/* Map container */}
      <div id='map-container' ref={mapContainerRef} />
    </>
  )
}

export default App
```

4.  Add a `performCategorySearch` function to call the `SearchBoxCore` component.

This asynchronous function triggers a search by calling `SearchBoxCore`'s `category()` method. The selected category is passed as the first argument, and the current map bounds are passed in as part of the `options` object. The `options` object includes the `bbox` property to tell the Search Box API to limit the search results to the current map bounds. By default, the Search Box API returns only 10 matching POIs, but you can increase this number by passing a `limit` property in the options object.

The response data is a GeoJSON FeatureCollection, which you can log to the console to see the results. The code snippet shown below unpacks the `features` array from the response and sets it to the `searchResults` state variable. This will be used in the next step to display the search results on the map.

Calling `setSearchBounds` allows you to keep track of the bounds used for the last data fetch. This will be useful in a later step when you add a "search this area" button to trigger another search for the current map bounds.

```jsx
import { useState, useEffect, useRef } from 'react'
import mapboxgl from 'mapbox-gl'
import { SearchBoxCore, SessionToken } from '@mapbox/search-js-core'


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

const MAPBOX_ACCESS_TOKEN = "YOUR_MAPBOX_ACCESS_TOKEN" // replace

const DEFAULT_MAP_BOUNDS = [
  [-74.03189, 40.69684],
  [-73.98121, 40.72286]
]

function App() {
  const mapRef = useRef() // ref for the Map() instance
  const mapContainerRef = useRef() // ref for the map container DOM element
  const searchRef = useRef() // ref for the SearchBoxCore() instance

  const [searchCategory, setSearchCategory] = useState() // the selected category
  const [searchResults, setSearchResults] = useState([]) // an array of search results
  const [mapBounds, setMapBounds] = useState() // the current map bounds
  const [searchBounds, setSearchBounds] = useState() // the bounds of the search results

  // function to perform a category search using the SearchBoxCore() instance
  // uses the current map bounds and the selected category to search for points of interest
  const performCategorySearch = async () => {
    if (!searchCategory || !mapBounds) return;
    const { features } = await searchRef.current.category(searchCategory, { bbox: mapBounds, limit: 25 });
    setSearchResults(features);
    setSearchBounds(mapBounds);

    console.log('Search results:', features)
  };


  useEffect(() => {
    mapRef.current = new mapboxgl.Map({
      accessToken: MAPBOX_ACCESS_TOKEN, // set the Mapbox access token
      container: mapContainerRef.current, // display the map in this DOM element
      bounds: DEFAULT_MAP_BOUNDS, // set the initial map bounds
      minZoom: 13, // set the minimum zoom level to avoid zooming out too far
      config: {
        basemap: {
          showPointOfInterestLabels: false, // disable POI labels
        }
      },
    })

    // when the map is loaded, set mapBounds to the current map bounds
    mapRef.current.on('load', () => {
      setMapBounds(mapRef.current.getBounds().toArray())
    })

    // when the map moves, set mapBounds to the current map bounds
    mapRef.current.on('moveend', () => {
      setMapBounds(mapRef.current.getBounds().toArray())
    })

     // instantiate the search box
        searchRef.current = new SearchBoxCore({ accessToken: MAPBOX_ACCESS_TOKEN })
        new SessionToken();

    // cleanup function: remove the map when the component unmounts
    return () => {
      mapRef.current.remove()
    }
  }, [])

    // when the searchCategory changes, perform a category search
    useEffect(() => {
      performCategorySearch()
    }, [searchCategory])

  // configuration array for category search buttons
  const categoryButtons = [
    { label: "☕ Coffee", value: "coffee" },
    { label: "🍽️ Restaurants", value: "restaurant" },
    { label: "🍸 Bars", value: "bar" },
    { label: "🏨 Hotels", value: "hotel" },
    { label: "🏛️ Museums", value: "museum" }
  ]

  return (
    <>
      {/* Show category search buttons */}
      <div className="button-container">
        {categoryButtons.map(({ label, value }) => (
          <button
            key={value}
            onClick={() => setSearchCategory(value)}
            className={`category-button ${searchCategory === value && 'active'}`}
          >
            {label}
          </button>
        ))}
      </div>

      {/* Map container */}
      <div id='map-container' ref={mapContainerRef} />
    </>
  )
}

export default App
```

5.  Add a `useEffect` hook to trigger the search when the user clicks a category button.

When `searchCategory` changes, the `useEffect` hook will call the `performCategorySearch` function to fetch the data for the selected category.

```jsx
import { useState, useEffect, useRef } from 'react'
import mapboxgl from 'mapbox-gl'
import { SearchBoxCore, SessionToken } from '@mapbox/search-js-core'


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

const MAPBOX_ACCESS_TOKEN = "YOUR_MAPBOX_ACCESS_TOKEN" // replace

const DEFAULT_MAP_BOUNDS = [
  [-74.03189, 40.69684],
  [-73.98121, 40.72286]
]

function App() {
  const mapRef = useRef() // ref for the Map() instance
  const mapContainerRef = useRef() // ref for the map container DOM element
  const searchRef = useRef() // ref for the SearchBoxCore() instance

  const [searchCategory, setSearchCategory] = useState() // the selected category
  const [searchResults, setSearchResults] = useState([]) // an array of search results
  const [mapBounds, setMapBounds] = useState() // the current map bounds
  const [searchBounds, setSearchBounds] = useState() // the bounds of the search results

  // function to perform a category search using the SearchBoxCore() instance
  // uses the current map bounds and the selected category to search for points of interest
  const performCategorySearch = async () => {
    if (!searchCategory || !mapBounds) return;
    const { features } = await searchRef.current.category(searchCategory, { bbox: mapBounds, limit: 25 });
    setSearchResults(features);
    setSearchBounds(mapBounds);

    console.log('Search results:', features)
  };


  useEffect(() => {
    mapRef.current = new mapboxgl.Map({
      accessToken: MAPBOX_ACCESS_TOKEN, // set the Mapbox access token
      container: mapContainerRef.current, // display the map in this DOM element
      bounds: DEFAULT_MAP_BOUNDS, // set the initial map bounds
      minZoom: 13, // set the minimum zoom level to avoid zooming out too far
      config: {
        basemap: {
          showPointOfInterestLabels: false, // disable POI labels
        }
      },
    })

    // when the map is loaded, set mapBounds to the current map bounds
    mapRef.current.on('load', () => {
      setMapBounds(mapRef.current.getBounds().toArray())
    })

    // when the map moves, set mapBounds to the current map bounds
    mapRef.current.on('moveend', () => {
      setMapBounds(mapRef.current.getBounds().toArray())
    })

     // instantiate the search box
        searchRef.current = new SearchBoxCore({ accessToken: MAPBOX_ACCESS_TOKEN })
        new SessionToken();

    // cleanup function: remove the map when the component unmounts
    return () => {
      mapRef.current.remove()
    }
  }, [])

    // when the searchCategory changes, perform a category search
    useEffect(() => {
      performCategorySearch()
    }, [searchCategory])

  // configuration array for category search buttons
  const categoryButtons = [
    { label: "☕ Coffee", value: "coffee" },
    { label: "🍽️ Restaurants", value: "restaurant" },
    { label: "🍸 Bars", value: "bar" },
    { label: "🏨 Hotels", value: "hotel" },
    { label: "🏛️ Museums", value: "museum" }
  ]

  return (
    <>
      {/* Show category search buttons */}
      <div className="button-container">
        {categoryButtons.map(({ label, value }) => (
          <button
            key={value}
            onClick={() => setSearchCategory(value)}
            className={`category-button ${searchCategory === value && 'active'}`}
          >
            {label}
          </button>
        ))}
      </div>

      {/* Map container */}
      <div id='map-container' ref={mapContainerRef} />
    </>
  )
}

export default App
```

With these changes in place, run your development server. For now, you can use your browser's developer tools to inspect the network traffic to see that the search request is working, and log the results to the console. Click a category button to trigger a search, and inspect the network request to see the results. You should see a request to the Search Box API with the selected category and the current map bounds.

Click another category button to trigger another search, and inspect the network requests again. You should see a new request to the Search Box API with a different category.

![A screen capture showing inspection of the network request to the Search Box API](https://docs.mapbox.com/help/ja/assets/ideal-img/tutorials--poi-search-react--4.480.gif)

## Display the search results with Markers

With search data flowing, you can now display the search results on the map. To do this, you will create a component to manage the lifecycle of a single [`mapboxgl.Marker`](https://docs.mapbox.com/mapbox-gl-js/api/markers/#marker) instance, and add it to the map at the location of each search result.

Add `POIMarker.jsx` to the `src` directory of your React app. This component receives the `Map` instance, a search result as a GeoJSON feature, and the selected category as props. It creates a new `mapboxgl.Marker` instance and adds it to the map at the location of the search result. The `POIMarker` component also adds a click event listener to show a [mapboxgl.Popup](https://docs.mapbox.com/mapbox-gl-js/api/markers/#popup) with additional information about the POI when the marker is clicked.

This component uses [React portals](https://react.dev/reference/react-dom/createPortal) to render the marker and popup content. The `POIMarker` component uses React's `createPortal` to render the marker and popup content into the DOM nodes created in the component. This allows you to use React components to create the marker and popup content, while still using Mapbox GL JS to manage the map and markers.

It also displays a custom SVG icon for the marker, and uses the `category` prop to determine which an emoji icon to display on the SVG.

```jsx
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import mapboxgl from "mapbox-gl";

const MarkerSVG = () => (
    <svg width="32" height="40" viewBox="0 0 88 106" fill="none" xmlns="http://www.w3.org/2000/svg">
        <g filter="url(#filter0_d_2001_2)">
            <path d="M84.5254 40.7407C84.5254 63.2412 54.0169 100 43.8475 100C32.7535 100 3.16949 63.2412 3.16949 40.7407C3.16949 18.2403 21.3816 0 43.8475 0C66.3133 0 84.5254 18.2403 84.5254 40.7407Z" fill="#43538D" />
        </g>
        <circle cx="43.8983" cy="40.8983" r="33.8983" fill="#6B82D6" />
        <defs>
            <filter id="filter0_d_2001_2" x="0.169495" y="0" width="87.3559" height="106" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
                <feFlood floodOpacity="0" result="BackgroundImageFix" />
                <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
                <feOffset dy="3" />
                <feGaussianBlur stdDeviation="1.5" />
                <feComposite in2="hardAlpha" operator="out" />
                <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
                <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2001_2" />
                <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2001_2" result="shape" />
            </filter>
        </defs>
    </svg>
)

// marker component receives a Map() instance, a GeoJSON point feature representing a Point of Interest, and a category string to determine which icon to display in the marker
const POIMarker = ({ map, feature, category }) => {
    const { geometry, properties } = feature;

    // ref for the Marker() instance, used to manage the marker lifecycle and remove it from the map
    const markerRef = useRef(null);
    // store the category in a state variable to use only its initial value
    const [emojiCategory] = useState(category);

    // refs for the DOM nodes to be used for the marker and popup content
    const markerContentRef = useRef(document.createElement("div"));
    const popupContentRef = useRef(document.createElement("div"));

    // when the component mounts, create a new marker and popup and add them to the map
    useEffect(() => {
        // instantiate a new Mapbox Popup()
        const popup = new mapboxgl.Popup({
            closeButton: false,
            closeOnMove: true,
            offset: 40,
        })
            .setDOMContent(popupContentRef.current) // use the popupContentRef as the content of the popup

        // instantiate a new Mapbox Marker()
        markerRef.current = new mapboxgl.Marker(markerContentRef.current, { // use the markerContentRef as the content of the marker
            anchor: "bottom",
        })
            .setLngLat(geometry.coordinates) // set the marker's position using the coordinates from the feature
            .setPopup(popup) // set the popup to be displayed when the marker is clicked
            .addTo(map); // add the marker to the map

        // cleanup function: remove marker when component unmounts
        return () => {
            markerRef.current.remove();
        };
    }, []);

    // assign an emoji based on the category
    let emoji;
    switch (emojiCategory) {
        case "restaurant":
            emoji = "🍽️";
            break;
        case "coffee":
            emoji = "☕";
            break;
        case "bar":
            emoji = "🍸";
            break;
        case "hotel":
            emoji = "🏨";
            break;
        case "museum":
            emoji = "🏛️";
            break;
        default:
            emoji = "📍"; // default icon if category doesn't match known types
    }

    // render nothing directly into the component tree,
    // instead, use React portals to inject content into the DOM nodes displayed in the marker and popup.
    return (
        <>
            {/* Portal 1: Popup content, rendered into the DOM node for the popup */}
            {createPortal(
                <div>
                    <div className="popup-title">{properties.name}</div>
                    <div className="popup-address">{properties.full_address}</div>
                </div>,
                popupContentRef.current
            )}

            {/* Portal 2: Marker content, rendered into the DOM node for the marker */}
            {createPortal(
                <>
                    <MarkerSVG />
                    <div className="marker-emoji">{emoji}</div>
                </>,
                markerContentRef.current
            )}
        </>
    );
};

export default POIMarker;
```

Add some CSS to style the marker and popup. The following CSS styles the marker and popup content, including the emoji icon, the marker label, and the popup title and address.

```css
...
.popup-title {
  font-weight: bold;
}

.popup-address {
  color: #444;
}

.marker-emoji {
  position: absolute;
  top: 6px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 15px;
}
```

In `App.jsx`, import `POIMarker` and iterate over the `searchResults` state variable, rendering a `POIMarker` for each feature in the search results. Pass the `map`, `feature`, and `searchCategory` props to `Marker` component. Be sure to only render the `POIMarker` component if there are search results.

```jsx
import { useState, useEffect, useRef } from 'react'
import mapboxgl from 'mapbox-gl'
import { SearchBoxCore, SessionToken } from '@mapbox/search-js-core'

import POIMarker from './POIMarker'

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

const MAPBOX_ACCESS_TOKEN = "YOUR_MAPBOX_ACCESS_TOKEN" // replace with your Mapbox access token

const DEFAULT_MAP_BOUNDS = [
    [-74.03189, 40.69684],
    [-73.98121, 40.72286]
]

function App() {
    const mapRef = useRef() // ref for the Map() instance
    const mapContainerRef = useRef() // ref for the map container DOM element
    const searchRef = useRef() // ref for the SearchBoxCore() instance

    const [searchCategory, setSearchCategory] = useState() // the selected category
    const [searchResults, setSearchResults] = useState([]) // an array of search results
    const [mapBounds, setMapBounds] = useState() // the current map bounds
    const [searchBounds, setSearchBounds] = useState() // the bounds of the search results

    // function to perform a category search using the SearchBoxCore() instance
    // uses the current map bounds and the selected category to search for points of interest
    const performCategorySearch = async () => {
        if (!searchCategory || !mapBounds) return;
        const { features } = await searchRef.current.category(searchCategory, { bbox: mapBounds, limit: 25 });
        setSearchResults(features);
        setSearchBounds(mapBounds);

        console.log('Search results:', features)
    };


    useEffect(() => {
        mapRef.current = new mapboxgl.Map({
            accessToken: MAPBOX_ACCESS_TOKEN, // set the Mapbox access token
            container: mapContainerRef.current, // display the map in this DOM element
            bounds: DEFAULT_MAP_BOUNDS, // set the initial map bounds
            minZoom: 13, // set the minimum zoom level to avoid zooming out too far
            config: {
                basemap: {
                    showPointOfInterestLabels: false, // disable POI labels
                }
            },
        })

        // when the map is loaded, set mapBounds to the current map bounds
        mapRef.current.on('load', () => {
            setMapBounds(mapRef.current.getBounds().toArray())
        })

        // when the map moves, set mapBounds to the current map bounds
        mapRef.current.on('moveend', () => {
            setMapBounds(mapRef.current.getBounds().toArray())
        })

        // instantiate the search box
        searchRef.current = new SearchBoxCore({ accessToken: MAPBOX_ACCESS_TOKEN })
        new SessionToken();

        // cleanup function: remove the map when the component unmounts
        return () => {
            mapRef.current.remove()
        }
    }, [])

    // when the searchCategory changes, perform a category search
    useEffect(() => {
        performCategorySearch()
    }, [searchCategory])

    // configuration array for category search buttons
    const categoryButtons = [
        { label: "☕ Coffee", value: "coffee" },
        { label: "🍽️ Restaurants", value: "restaurant" },
        { label: "🍸 Bars", value: "bar" },
        { label: "🏨 Hotels", value: "hotel" },
        { label: "🏛️ Museums", value: "museum" }
    ]

    return (
        <>
            {/* Show category search buttons */}
            <div className="button-container">
                {categoryButtons.map(({ label, value }) => (
                    <button
                        key={value}
                        onClick={() => setSearchCategory(value)}
                        className={`category-button ${searchCategory === value && 'active'}`}
                    >
                        {label}
                    </button>
                ))}
            </div>

            {/* Map container */}
            <div id='map-container' ref={mapContainerRef} />

            {/* render a POIMarker for each feature in searchResults */}
            {searchResults.length > 0 && searchResults.map((feature) => (
                <POIMarker key={feature.properties.mapbox_id} map={mapRef.current} feature={feature} category={searchCategory} />
            ))}
        </>
    )
}

export default App
```

Run your app and click a category button to trigger a search. You will see markers appear on the map for each point of interest in the search results. Clicking a marker will show a popup with additional information about the POI. Drag or zoom the map, and choose a different category to trigger a new search. Because you stored the map bounds each time the map moves, each search action is always using the current bounds to limit results.

![A screen capture showing inspection of the network request to the Search Box API](https://docs.mapbox.com/help/ja/assets/ideal-img/tutorials--poi-search-react--5.480.gif)

In the next step, you will add a "search this area" button to trigger another search for the current category if the user moves the map.

## Add a 'Search this area' button

When the user clicks a category button, the app fetches POI data for the current map bounds. But if the user moves the map after search results are displayed, the app does not automatically update the search results. You don't necessary want to fetch new search results on each map move, as the user may be exploring the map and may not want to lose the points of interest already displayed on the map. A "search this area" button allows the user to trigger a new search for the current map bounds when they are ready.

You don't always want this button to be visible, so this step adds some logic to determine when to show it. You want it to show only when the user has moved the map and its current bounds no longer match the bounds used to fetch the data.

1.  Near your other `useState` declarations, add a new boolean state variable `showSearchThisArea` to set the visibility of the button.

```jsx
...
  const [showSearchAreaButton, setShowSearchAreaButton] = useState(false) // show the "search this area" button
...
```

2.  Add another `useEffect` to determine when to show the button.

This effect is triggered when the `mapBounds`, `searchCategory`, or `searchBounds` state variables change. It checks whether the current map bounds are different from the bounds used for the last search. If they are, it sets `showSearchThisArea` to true, otherwise it sets it to false.

```jsx
...
  // determine whether to show the "Search this area" button based on the current map bounds and the bounds used for the most recent search
  // if the map bounds have changed since the last search, show the button
  useEffect(() => {
    function boundsChanged(boundsA, boundsB) {
      if (!boundsA || !boundsB) return false
      return JSON.stringify(boundsA) !== JSON.stringify(boundsB)
    }

    if (searchCategory && boundsChanged(mapBounds, searchBounds)) {
      setShowSearchAreaButton(true)
    } else {
      setShowSearchAreaButton(false)
    }
  }, [mapBounds, searchCategory, searchBounds])
...
```

In the return statement, include a button that triggers a new search based on the current map bounds. Display this button only when `showSearchThisArea` is true. When a user clicks the button, the app calls the `performCategorySearch` function using the current map bounds.

```jsx
...
return (
        <>
            {/* Show category search buttons */}
            <div className="button-container">
                {categoryButtons.map(({ label, value }) => (
                    <button
                        key={value}
                        onClick={() => setSearchCategory(value)}
                        className={`category-button ${searchCategory === value && 'active'}`}
                    >
                        {label}
                    </button>
                ))}
            </div>

            // highlight-start
            {/* Show "search this area" button */}
            {showSearchAreaButton && (
                <button
                    onClick={performCategorySearch}
                    className="search-area-button"
                >
                    Search this area
                </button>
            )}
            // highlight-end

            {/* Map container */}
            <div id='map-container' ref={mapContainerRef} />

            {/* render a POIMarker for each feature in searchResults */}
            {searchResults.length > 0 && searchResults.map((feature) => (
                <POIMarker key={feature.properties.mapbox_id} map={mapRef.current} feature={feature} category={searchCategory} />
            ))}
        </>
    )
...
```

Add some CSS to position this button in the top center of the map.

```css
...
.search-area-button {
  position: fixed; 
  top: 10px;
  left: 50%; 
  z-index: 1;
}

```

![A screen capture showing inspection of the network request to the Search Box API](https://docs.mapbox.com/help/ja/assets/ideal-img/tutorials--poi-search-react--6.480.gif)

## Finished product

With the "search this area" button in place, you have a robust implementation for searching for nearby points of interest in a React app, allowing the user to explore the map and fetch new data as needed. Compare your implementation to the finished product below.

## Next steps

**Congratulations!** You've successfully added a Point of Interest (POI) search feature to your React app using Mapbox GL JS and Mapbox Search JS. You can now search for nearby points of interest by category, display them on the map with custom markers, and show popups with additional information.

### What we covered

-   Build UI to trigger category search queries in a mapping application.
-   Use the `SearchBoxCore` component of [Mapbox Search JS](https://docs.mapbox.com/mapbox-search-js/api/core/search/#searchboxcore) to search for POIs.
-   Get the map's bounds and use them as a search option to limit results to the current map view.
-   Use a React component to add custom markers and popups to the map for each point of interest in the search results.
-   Add a "search this area" button to trigger another category search if the user moves the map.

### Things to try

With this minimal implementation working, you can try to add more features to your app:

-   **Loading State**: Add a loading spinner to show when data is being fetched.
-   **Custom Styling**: Use CSS or add a design framework to style the markers and popups to match your app's design.
-   **Try a different location**: Change the initial map bounds variable to a place you are more familiar with. Use our [Location Helper](https://labs.mapbox.com/location-helper/) tool to find the bounds of a location you want to use.
-   **Try different categories**: Update the category buttons to use different categories. You can discover available categories using the [Search Box API Playground](https://docs.mapbox.com/playground/search-box/suggest-retrieve/).
-   **Use a symbol layer for results**: Instead of using Mapbox GL JS `Marker` instances, you can use a symbol layer to display the search results on the map. This approach gives you more control over the styling and behavior of the points allowing for conflict detection of overlapping labels.

### Learn more

-   Browse the source code for this tutorial on [GitHub](https://github.com/mapbox/tutorials/tree/main/poi-search-react/)
    
-   Explore the [Mapbox Search JS documentation](https://docs.mapbox.com/mapbox-search-js/) to learn more about the `SearchBoxCore` component and other features.
    
-   See the [Mapbox Search Box API documentation](https://docs.mapbox.com/api/search/search-box/) to learn more about the available endpoints and options.
    
-   Check the [Mapbox GL JS documentation](https://docs.mapbox.com/mapbox-gl-js/) to learn more about the `Marker` component and other features.