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

# Add custom data to Mapbox Search JS

This tutorial outlines how to integrate custom data into [**Mapbox Search JS**](https://docs.mapbox.com/mapbox-search-js/), providing an interactive search experience in a web app that includes your own data as well as search results returned by Mapbox Search APIs. This tutorial will use the **Mapbox Search JS** core methods and classes and will feature a custom built search box UI built with **TailWind CSS**.

This tutorial will focus on an aviation use case. Mapbox Search returns major airports after searching their name or [IATA](https://en.wikipedia.org/wiki/IATA_airport_code) code (for example: searching 'John F Kennedy Airport' or 'JKF' return the airport search result). This is useful for most general search needs. But an aviation company would benefit from having not just the major international airports returned, but every airport, municpal, regional, private or otherwise returned in their results. In this tutorial you will add ~20,000 U.S. based airports to the application, and efficiently index them for searching by IATA code, allowing the interactive search of the application to return custom airport data as well as the normal Mapbox search results.

By the end of the this tutorial you will have:

-   Created a React application using the `npm create @mapbox/web-app` command
-   Imported custom airport data & created a hash map of the data for efficient indexing
-   Written your own search function to return results for Airports
-   Implement a custom Search Box component architecture using [`SearchBoxCore`](https://docs.mapbox.com/mapbox-search-js/api/core/search/#searchboxcore) and [`SearchSession`](https://docs.mapbox.com/mapbox-search-js/api/core/search_session/) to power the search and [TailWind CSS](https://tailwindcss.com/) to style the Search box and search suggestions
-   Handle search suggestion clicks with the [`SearchSession.retrieve`](https://docs.mapbox.com/mapbox-search-js/api/core/search_session/#searchsession#retrieve) and the [`flyTo`](https://docs.mapbox.com/mapbox-gl-js/api/map/#map#flyto) method.

Below you'll see an example video of the application, or try the [finished app here](https://docs.mapbox.com/help/ja/help/ja/demos/custom-data-with-search-js/final.html).

Your browser doesn't support HTML5 video. Open [link to the video](https://docs.mapbox.com/help/ja/help/ja/assets/medias/tutorials--custom-data-with-search-js--search-demo-cfbaae860692bcfa3227e2eccd636b2c.mp4).

## Prerequisites

There are a few resources you'll need before getting started:

-   **A Mapbox access token**: Find yours on the [Access token page](https://console.mapbox.com/account/access-tokens/) of your Developer Console.
-   **A code editor** like [Visual Studio Code](https://code.visualstudio.com/)
-   **Node.js and npm.** [Download and install](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) the latest version.
-   **Familiarity with a terminal** to execute node.js and npm commands.
-   **Familiarity with React development**: Beginner experience with JSX, CSS, HTML and JavaScript.

## Scaffold a React application

To start the project, you will use the `npm create @mapbox/web-app` developer tool to quickly scaffold a web app using React, Mapbox GL JS and Mapbox Search JS.

In your terminal run the command

```sh
$ npm create @mapbox/web-app
```

You will be prompted with a few questions by the developer tool:

1.  **Which framework do you want to use?** Select 'React'
2.  **Project name:** For this tutorial name your project `mapbox-custom-search`
3.  **Enter your Mapbox Access Token:** Paste in your access token from [console.mapbox.com](https://console.mapbox.com)
4.  **Addons: Add interactive Search to your map?** Select 'Y' for yes.

After answering the questions the CLI tool will scaffold a new web app, using [Vite](https://vite.dev), [**Mapbox GL JS**](https://docs.mapbox.com/mapbox-gl-js/) and [**Mapbox Search JS**](https://docs.mapbox.com/mapbox-search-js/), it will install dependencies, build the app and start the development server.

Your browser should open and you should see a map centered over Boston, MA with integrated interactive Search using **Mapbox Search JS**. Try searching for cities, addresses, points of interest and select a search result to see the `flyTo` behavior which navigates the map to the clicked search result.

![initial app state](https://docs.mapbox.com/help/ja/assets/ideal-img/tutorials--custom-data-with-search-js--initial-app.0012481.480.png)

## Add & prepare custom data

Now that you have a web app with interactive search running, it's time to bring in the custom data for search. For this tutorial, you'll use a GeoJSON file containing over 20,000 U.S. airports with their IATA codes, names, cities, and coordinates.

While this tutorial uses a local data source, the approach to integrate custom data could source that data from an outside API or outside data source as well.

### Download the airport data

Download the airport GeoJSON file and place it in your project's `public/` folder:

[Download US_Airports.geojson](https://docs.mapbox.com/help/ja/help/data/US_Airports.geojson)

The GeoJSON contains airport features with the following properties:

-   `IDENT`: The airport's IATA code (e.g., "JFK", "LAX")
-   `NAME`: Full airport name
-   `SERVCITY`: The city the airport serves
-   `STATE`: State abbreviation
-   Plus geometry coordinates for mapping

### Create the search utilities file

Next you will create a new file to handle indexing the airport data:

1.  Open the `src` folder and create a new folder named `utils`.
2.  In the `utils` folder, create a new file named `search.js`, and paste in the code below.

```javascript
export function buildAirportIndex(airportData) {
  const iataIndex = new Map();
  
  if (!airportData?.features) return iataIndex;
  
  for (const feature of airportData.features) {
    const iata = feature.properties.IDENT;
    if (iata) {
      // Only index 2-char, 3-char, and 4-char prefixes
      for (let i = 2; i <= Math.min(iata.length, 4); i++) {
        const prefix = iata.substring(0, i);
        if (!iataIndex.has(prefix)) {
          iataIndex.set(prefix, []);
        }
        iataIndex.get(prefix).push(feature);
      }
    }
  }
  
  return iataIndex;
}
```

The `buildAirportIndex` function creates a hash map (JavaScript [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/Map)) that indexes airports by their IATA code prefixes. This allows for instant look ups when a user types 2-4 characters.

Details

**Why use an index?**

-   **Performance**: Instead of searching through 20,000+ airports on every keystroke, the index provides O(1) lookup time
-   **Prefix matching**: Indexes 2-4 character prefixes so typing "JF" immediately returns all airports starting with "JF"
-   **Memory efficient**: Only creates index entries for valid IATA codes
-   **Signal strength**: We don't index single characters because that would return too many results and waste computation—waiting for at least 2 characters provides better signal about what the user is searching for

### Load and index the airport data

Now update your `App.jsx` to load the airport data when the component mounts. Review the code snippet below and copy the highlighted changes into your `App.jsx` file.

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

import 'mapbox-gl/dist/mapbox-gl.css';
import './App.css'
// highlight-start
import { buildAirportIndex } from './utils/search'
// highlight-end

const accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;
const center = [-71.05953, 42.36290];

function App() {
  const mapRef = useRef()
  const mapContainerRef = useRef()
  // highlight-start
  const airportDataRef = useRef(null)
  const [airportIndex, setAirportIndex] = useState(null)
  // highlight-end
  const [inputValue, setInputValue] = useState("");

  useEffect(() => {
     mapRef.current = new mapboxgl.Map({
      accessToken: accessToken,
      container: mapContainerRef.current,
      center:  center,
      zoom: 13,
    });

    // highlight-start
    // Load airport data and build index
    const loadAirportData = async () => {
      try {
        const res = await fetch('./US_Airports.geojson')
        const json = await res.json()
        
        airportDataRef.current = json
        const index = buildAirportIndex(json)
        setAirportIndex(index)
        
        console.log('Airport index built:', index.size, 'prefixes indexed')
      } catch(err) {
        console.error("Error fetching airport data:", err)
      }
    }

    loadAirportData();
    // highlight-end
    return () => {
      mapRef.current.remove()
    }
  }, [])

  return (
    <>
      <div style={{
          margin: '10px 10px 0 0',
          width: 300,
          right: 0,
          top: 0,
          position: 'absolute',
          zIndex: 10 }}>
          <SearchBox
              accessToken={accessToken}
              map={mapRef.current}
              mapboxgl={mapboxgl}
              value={inputValue}
              proximity={center}
              onChange={(d) => {
              setInputValue(d);
              }}
              marker
          />
      </div>
      <div id='map-container' ref={mapContainerRef} />
    </>
  )
}

export default App
```

**Key changes:**

-   Imported `buildAirportIndex` from the utilities file
-   Created a ref to store the airport data and a state variable to store the index (we use a ref to store the airport data as this will only be needed once and we use state for the index as this will be passed into a child component later)
-   Added an `async` function `loadAirportData` that fetches the GeoJSON and builds the index
-   Called `loadAirportData` inside the existing `useEffect` hook

Reload your app and check the browser console—you should see a message indicating how many IATA code prefixes were indexed.

```sh
Airport index built: 23625 prefixes indexed
```

## Write custom search function

Now that you have an indexed dataset, you'll create a search function that uses the index to quickly find matching airports based on user input.

### Add the `searchAirports` function

Open `src/utils/search.js` and add the `searchAirports` function and helper below the `buildAirportIndex` function:

```javascript
...

export async function searchAirports(query, iataIndex, maxResults = 5) {
  if(!query || query.length < 2) return []; // Only search if query is 2+ chars

  const q = query.toUpperCase().trim()
  
  // Use index for IATA lookup (super fast)
  if(q.length <= 4 && iataIndex) {
    const matches = iataIndex.get(q) || []
    
    return matches  
      .slice(0, maxResults)
      .map(formatAirportResult)
  }
  
  // For longer queries, no airport search (could add name/city search later)
  return [];
}

function formatAirportResult(feature) {
  const props = feature.properties;
  return {
    name: `${props.IDENT} - ${props.NAME}`,
    place_formatted: `${props.SERVCITY}, ${props.STATE}`,
    mapbox_id: `airport_${props.IDENT}`,
    feature_type: 'airport',
    coordinates: feature.geometry.coordinates,
    original_data: feature
  }
}
```

### How `searchAirports` works

1.  **Input validation**: The function returns an empty array if query is less than 2 characters (avoids too many results)
2.  **Query normalization**: Converts to uppercase and trims whitespace to match IATA code format
3.  **Index lookup**: Uses the hash map to instantly retrieve all airports matching the prefix
4.  **Result limiting**: Limits the results based on a `maxResults` parameter (defaults to `5`)
5.  **Format conversion**: Transforms GeoJSON features into a format compatible with Mapbox Search results

The `formatAirportResult` helper creates a consistent structure that matches the **Mapbox Search Box API** response format. This makes it easier to blend the custom data and Mapbox results later.

### Test the search function

Update `App.jsx` to import and test the `searchAirports` function by adding a temporary test inside the `loadAirportData` function. Update your `App.jsx` with the highlighted snippets below.

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

import 'mapbox-gl/dist/mapbox-gl.css';
import './App.css'
// highlight-start
import { buildAirportIndex, searchAirports } from './utils/search' // add searchAirports
// highlight-end
const accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;
const center = [-71.05953, 42.36290];

function App() {
  const mapRef = useRef()
  const mapContainerRef = useRef()
  const airportDataRef = useRef(null)
  const [airportIndex, setAirportIndex] = useState(null)
  const [inputValue, setInputValue] = useState("");

  useEffect(() => {
     mapRef.current = new mapboxgl.Map({
      accessToken: accessToken,
      container: mapContainerRef.current,
      center:  center,
      zoom: 13,
    });

    // Load airport data and build index
    const loadAirportData = async () => {
      try {
        const res = await fetch('./US_Airports.geojson')
        const json = await res.json()
        
        airportDataRef.current = json
        const index = buildAirportIndex(json)
        setAirportIndex(index)
    
        // highlight-start
        // Test the search function
        const testResults = await searchAirports('JFK', index, 3)
        console.log('Search results for "JFK":', testResults)
        // highlight-end
      } catch(err) {
        console.error("Error fetching airport data:", err)
      }
    }

    loadAirportData();

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

  return (
    <>
     ...
    </>
  )
}

export default App
```

Reload your app and check the browser console. You should see results for "JFK" showing airports with IATA codes starting with those letters, formatted with name, city, state, and coordinates.

**What you'll see:**

```js
Search results for "JFK": [
  {
    "name": "JFK - John F Kennedy Intl",
    "place_formatted": "NEW YORK, NY",
    "mapbox_id": "airport_JFK",
    "feature_type": "airport",
    "coordinates": [
      -73.7786935933282,
      40.639936306032,
      0
    ],
    "original_data": {
      "type": "Feature",
      ...
    }
  }
]
```

The search function is now ready! In the next steps, you'll build a custom UI to display these results alongside Mapbox Search results.

## Building a custom search UI

The pre-built [`SearchBox`](https://docs.mapbox.com/mapbox-search-js/api/react/search/) component from `@mapbox/search-js-react` is great for a quick interactive search implementation. It also allows for custom search data to be passed into the existing `SearchBox` component via the [`customSearch`](https://docs.mapbox.com/mapbox-search-js/api/web/search/#mapboxsearchboxcomponentoptions) parameter. While this is a powerful parameter to extend search functionality, to have control to add custom icons, styles and display logic to the `SearchBox` UI and the suggestions list, you'll need to build your own search UI. This will give you full control over how results are displayed and how user interactions are handled.

### Install Tailwind CSS

To start, you'll install Tailwind CSS to use for styling your custom components. Run the following command in your console:

```bash
$ npm install -D tailwindcss @tailwindcss/vite
```

Update your `vite.config.js` to include the Tailwind Vite plugin:

```javascript
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
  ],
})
```

Add Tailwind's directives to your `src/index.css`:

```css
@import "tailwindcss";
```

Now that TailwindCSS is installed and ready to use in the project, in the next step you'll create a new `SearchBoxContainer` component to replace the existing `SearchBox` from **Mapbox Search JS**.

## Create the `SearchBoxContainer` component

This component will manage the search logic, API requests and state management for our new search box. This component imports [`SearchBoxCore`](https://docs.mapbox.com/mapbox-search-js/api/core/search/#searchboxcore) and [`SearchSession`](https://docs.mapbox.com/mapbox-search-js/api/core/search_session/) dependencies from `@mapbox/search-js-core` which are classes that will be used to manage the interactions with Search Box API.

Create `src/SearchBoxContainer.jsx` and paste in the following code:

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

const SearchBoxContainer = ({mapRef}) => {
  const [searchInput, setSearchInput] = useState('')
  const [suggestions, setSuggestions] = useState([])
  const [selectedResult, setSelectedResult] = useState(null)
  const sessionRef = useRef(null)
  const markerRef = useRef(null)

  const handleChange = (e) => {
    setSearchInput(e.target.value)
  }

  useEffect(() => {
    // Initialize Search Core and Session
    const search = new SearchBoxCore({ 
      accessToken: import.meta.env.VITE_MAPBOX_ACCESS_TOKEN 
    })
    const session = new SearchSession(search)
    
    sessionRef.current = session
    // Instiantiate a Mapbox Marker to attach to selected results (see useEffect below)
    markerRef.current = new mapboxgl.Marker()
  }, [])

  useEffect(() => {
    if(!searchInput) {
      setSuggestions([])
      return
    }

    // Debounce search - wait 300ms after user stops typing
    const timeoutId = setTimeout(async () => {
      try {
        const searchBoxResults = await sessionRef.current.suggest(searchInput, {
          types: ['address', 'place', 'street', 'poi', 'city', 'locality', 'country']
        })
        
        if (searchBoxResults?.suggestions.length > 0) {
          console.log("suggestions:", searchBoxResults.suggestions)
          setSuggestions(searchBoxResults.suggestions)
        }
      } catch(err) {
        console.error("Search error:", err)
      }
    }, 300)

    return () => clearTimeout(timeoutId)
  }, [searchInput])

  useEffect(() => {
    if(!selectedResult) return

    async function retrieveSuggestion() {
      if(sessionRef.current.canRetrieve(selectedResult)) {
        const { features } = await sessionRef.current.retrieve(selectedResult)
        const feature = features[0]

        // Fly map to result
        mapRef.current.flyTo({
          center: feature.geometry.coordinates,
          zoom: 16
        })

        // Add marker
        markerRef.current
          .setLngLat(feature.geometry.coordinates)
          .addTo(mapRef.current)
      }
    }
    
    retrieveSuggestion()
    setSearchInput('') // Clear input after selection
  }, [selectedResult])

  return (
    <div>
      {/* CustomSearchBox component will go here */}
      <input 
        className="bg-white rounded p-2" 
        type="text" 
        value={searchInput}
        onChange={handleChange}
        placeholder="Search..."
      />
    </div>
  )
}

export default SearchBoxContainer
```

### Understanding the component architecture

The `SearchBoxContainer` component orchestrates all the search functionality without providing any UI—that's the job of the child components you'll create next. Below is a break down on how this container component manages the search experience:

#### State management

The component maintains three pieces of React state:

-   **`searchInput`**: Tracks the current value of the search input field
-   **`suggestions`**: Stores the array of search results to display
-   **`selectedResult`**: Holds the suggestion the user clicked on

It also uses two refs to persist objects across renders without triggering re-renders:

-   **`sessionRef`**: Stores the `SearchSession` instance that manages search state and API communication
-   **`markerRef`**: Stores the Mapbox GL JS marker that appears when a result is selected

#### Initialization (first useEffect)

When the component mounts, it creates:

1.  A `SearchBoxCore` instance—this is the engine that communicates with [**Search Box API**](https://docs.mapbox.com/api/search/search-box/)
2.  A [`SearchSession`](https://docs.mapbox.com/mapbox-search-js/api/core/search_session/) instance—the `searchSession` object is a managed entry point to the **Search Box API**. It abstracts the suggest/retrieve flow of the two-step interactive search experience and manages search sessions.
3.  A `mapboxgl.Marker` instance—ready to be placed on the map when results are selected

#### Search logic (second useEffect)

This effect runs whenever `searchInput` changes:

1.  **Early return**: If the input is empty, it clears suggestions and stops
2.  **Debouncing**: Uses `setTimeout` to wait 300 milliseconds after the user stops typing before making an API request—this prevents excessive API calls while typing rapidly
3.  **API request**: Calls `sessionRef.current.suggest()` with the search query and configuration options limiting to specific feature types like addresses, cities, and POIs. For more options available for the suggest endpoint see the [API reference docs](https://docs.mapbox.com/api/search/search-box/#get-suggested-results).
4.  **Update state**: Sets the suggestions array to display in the UI
5.  **Cleanup**: Returns a cleanup function that cancels the timeout if the component unmounts or if `searchInput` changes again before 300 milliseconds passes

#### Result selection (third useEffect)

This effect runs when `selectedResult` changes (when a user clicks a suggestion):

1.  **Validation**: Checks if there's a selected result
2.  **Feature retrieval**: Calls `sessionRef.current.retrieve()` to get the full GeoJSON feature data for the selected suggestion (suggestions are lightweight; full features contain complete geometry and properties)
3.  **Map navigation**: Uses `mapRef.current.flyTo()` to smoothly animate the map to the result's coordinates
4.  **Marker placement**: Positions the marker at the result's location and adds it to the map
5.  **Input reset**: Clears the search input after selection for a clean slate

This top level container component handles all the complex logic and state management and passes this state into an `input` field in the return.

Now that the new components are created, next you'll update the main `App.jsx` and `SearchBoxContainer.jsx` to wire up the new components.

### Update App.jsx to use the new `SearchBoxContainer`

In your `App.jsx` update the file to remove the `SearchBox` component and add the new `SearchBoxContainer`. Note that `SearchboxContainer` includes a prop to receive the `airportIndexRef` as that will be needed in the next step. Additionally, remove the `searchInput` state variable, as this state is handled in `SearchBoxContainer`.

```diff
import { useRef, useEffect, useState } from 'react'
import mapboxgl from 'mapbox-gl'
- import { SearchBox } from '@mapbox-search-js/react'
+ import SearchBoxContainer from './SearchBoxContainer'

import 'mapbox-gl/dist/mapbox-gl.css';
import './App.css'
import { buildAirportIndex } from './utils/searchUtils'

const accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;
const center = [-71.05953, 42.36290];

function App() {
  const mapRef = useRef()
  const mapContainerRef = useRef()
  const airportDataRef = useRef(null)
  const [airportIndex, setAirportIndex] = useState(null)
-  const [inputValue, setInputValue] = useState("");

  useEffect(() => {
    ... 
    }
  }, [])

  return (
    <>
      <div style={{
          margin: '10px 10px 0 0',
          width: 300,
          right: 0,
          top: 0,
          position: 'absolute',
          zIndex: 10 }}>
-            <SearchBox
-              accessToken={accessToken}
-              map={mapRef.current}
-              mapboxgl={mapboxgl}
-              value={inputValue}
-              proximity={center}
-              onChange={(d) => {
-              setInputValue(d);
-              }}
-              marker
-             />
+         <SearchBoxContainer 
+            mapRef={mapRef}
+            airportIndex={airportIndexRef.current}/>
      </div>
      <div id='map-container' ref={mapContainerRef} />
    </>
  )
}

export default App
```

Now that you have wired up the new `SearchBoxContainer` when you run your app, you should see a new input styled with **TailWind CSS** classes in the top right. Enter a search query into the input and you will see 5 results (suggestions) logged to the console.

![Suggestions logged to console](https://docs.mapbox.com/help/ja/assets/ideal-img/tutorials--custom-data-with-search-js--search-suggestions-log.12c9bcc.480.png)

You have successfully integrated search functionality with the **Mapbox Search JS** core methods `SearchBoxCore` and `SearchSession`. In the next step you'll replace this input field with child components to create the 2 step search UI, rendering a suggestions list and handling interactivity.

## Create the custom Search UI components

In this section you'll create a `SearchBox` component which renders the search input of the UI and a `SearchSuggestion` component which renders a search result view, which is displayed when a user make a query.

### Create the `SearchBox` component

Create `src/SearchBox.jsx` for the search input and suggestions display:

```jsx
import React from "react"
import SearchSuggestion from './SearchSuggestion'

const SearchBox = ({searchInput, handleChange, suggestions, setSelectedResult}) => {
  return (
    <div>
      <input 
        type="text" 
        className="bg-white rounded-lg border border-gray-400 p-3 w-full shadow-sm focus:outline-none focus:ring focus:ring-blue-500 focus:border-blue-500 transition-all placeholder-gray-400 text-gray-700"
        value={searchInput}
        onChange={handleChange}
        placeholder="Search..."
      />

      {suggestions.length > 0 && (
        <div className="bg-white rounded-lg border border-gray-400 w-full shadow-sm mt-1">
          {suggestions.map((suggestion, index) => (
               <SearchSuggestion 
                  key={index}
                  suggestion={suggestion}
                  setSelectedResult={setSelectedResult}/>
          ))}
        </div>
      )}
    </div>
  )
}

export default SearchBox
```

#### Understanding `SearchBox`

The `SearchBox` component uses Tailwind CSS classes for styling. The component contains a text `input` to manage the search. This receives the `searchInput` state and the `handleChange` function from `SearchBoxContainer`. If `suggestions` exist, it `map`s over the suggestions and returns a `SearchSuggestion` component you will create next.

### Create the `SearchSuggestion` component

Create the `src/SearchSuggestion.jsx` and paste in the following snippet:

```jsx
import React from "react"

const SearchSuggestion = ({suggestion, setSelectedResult}) => {
  return (
    <div 
      className="flex flex-col hover:bg-gray-200 hover:cursor-pointer px-3 py-2"
      onClick={() => setSelectedResult(suggestion)}>
        <div className="flex items-center">
          <div className="font-bold text-sm">{suggestion.name}</div>
        </div>
        <div className="text-[12px]">{suggestion.place_formatted}</div>
    </div>
  )
};

export default SearchSuggestion;
```

The `SearchSuggestion` component receives the `suggestion` prop, and the `setSelectedResult` state function. It renders a suggestion name and place, and passes the `setSelectedResult` state function to the `onClick` handler, setting the current suggestion as the selected result when clicked.

### Update `SearchBoxContainer` to use the new `SearchBox` component

You will need to replace the generic input component with your custom `SearchBoxContainer`.

Add the import for `SearchBox` and update the return statement in `SearchBoxContainer.jsx` as seen below:

```diff
import React, { useState, useEffect, useRef } from "react"
+ import SearchBox from "./SearchBox"
import { SearchBoxCore, SearchSession } from "@mapbox/search-js-core"
import mapboxgl from 'mapbox-gl'

const SearchBoxContainer = ({mapRef}) => {
  const [searchInput, setSearchInput] = useState('')
  const [suggestions, setSuggestions] = useState([])
  const [selectedResult, setSelectedResult] = useState(null)
  const sessionRef = useRef(null)
  const markerRef = useRef(null)

  // ... (rest of the component code stays the same)

  return (
    <div>
-     {/* New SearchBox component will go here */}
-      <input 
-        type="text" 
-        value={searchInput}
-        onChange={handleChange}
-        placeholder="Search..."
-      />
+      <SearchBox 
+        searchInput={searchInput} 
+        handleChange={handleChange}
+        suggestions={suggestions} 
+        setSelectedResult={setSelectedResult}
+      />
    </div>
  )
}

export default SearchBoxContainer
```

Reload your app and test the custom search box. You should now see a styled search input that fetches and displays Mapbox search results. Try searching for cities, addresses, or POIs—the results will appear in your custom UI and clicking them will fly the map to that location.

Your browser doesn't support HTML5 video. Open [link to the video](https://docs.mapbox.com/help/ja/help/ja/assets/medias/tutorials--custom-data-with-search-js--custom-search-ui-80b72a345a42c26e0dcec0b13c1e6920.mp4).

In the next section, you'll integrate your custom airport data into this search experience.

## Add custom data to search results

Now comes the exciting part—integrating your custom airport data with Mapbox search results to create a unified search experience!

### Merge airport and Mapbox results

Update `SearchBoxContainer.jsx` to search both your airport data and Mapbox simultaneously, then merge the results:

```jsx
import React, { useState, useEffect, useRef } from "react"
import CustomSearchBox from "./CustomSearchBox"
import { SearchBoxCore, SearchSession } from "@mapbox/search-js-core"
// highlight-start
import { searchAirports } from "./utils/search"
// highlight-end
import mapboxgl from 'mapbox-gl'

// highlight-start
const SearchBoxContainer = ({mapRef, airportIndex}) => {
  // highlight-end
  const [searchInput, setSearchInput] = useState('')
  const [suggestions, setSuggestions] = useState([])
  const [selectedResult, setSelectedResult] = useState(null)
  const sessionRef = useRef(null)
  const markerRef = useRef(null)

  const handleChange = (e) => {
    setSearchInput(e.target.value)
  }

  useEffect(() => {
    const search = new SearchBoxCore({ 
      accessToken: import.meta.env.VITE_MAPBOX_ACCESS_TOKEN 
    })
    const session = new SearchSession(search)
    
    sessionRef.current = session
    markerRef.current = new mapboxgl.Marker()
  }, [])

  useEffect(() => {
    if(!searchInput) {
      setSuggestions([])
      return
    }

    const timeoutId = setTimeout(async () => {
      try {
        // highlight-start
        // Search both sources in parallel
        const [searchBoxResults, airportResults] = await Promise.all([
          sessionRef.current.suggest(searchInput, {
            types: ['address', 'place', 'street', 'poi', 'city', 'locality', 'country']
          }),
          searchAirports(searchInput, airportIndex, 5)
        ])
        
        if (searchBoxResults?.suggestions.length === 0 && airportResults.length === 0) {
          setSuggestions([])
          return
        }

        // Merge results: airports first, then Mapbox results
        const combined = [
          ...(airportResults || []),
          ...(searchBoxResults?.suggestions || [])
        ]

        setSuggestions(combined)
        // highlight-end
      } catch(err) {
        console.error("Search error:", err)
      }
    }, 300)

    return () => clearTimeout(timeoutId)
    // highlight-start
  }, [searchInput, airportIndex])
  // highlight-end

  useEffect(() => {
    if(!selectedResult) return

    async function retrieveSuggestion() {
      // highlight-start
      let feature
      // Handle airport result click - match structure of SearchBox Retrieve response
      if(selectedResult.feature_type == 'airport') {
        feature =  {
          type: 'Feature',
          properties: selectedResult,
          geometry: {
            type: 'Point',
            coordinates: selectedResult.coordinates
          }
        }
      } else {
        // Handle retrieve suggestion from Mapbox result
        if(sessionRef.current.canRetrieve(selectedResult)) {
        const { features } = await sessionRef.current.retrieve(selectedResult);
        feature = features[0]
        }
      }

      // Fly map to selectedResult
      mapRef.current.flyTo({
        center: feature.geometry.coordinates,
        zoom: 16
      })

      // Create a marker and add it to the map.
      markerRef.current.setLngLat(feature.geometry.coordinates).addTo(mapRef.current);
      // highlight-end
    }
    retrieveSuggestion();
    setSearchInput(''); // clear search input after retrieving suggestion

  }, [selectedResult])

  return (
    <div>
      <CustomSearchBox 
        searchInput={searchInput} 
        handleChange={handleChange}
        suggestions={suggestions} 
        setSelectedResult={setSelectedResult}
      />
    </div>
  )
}

export default SearchBoxContainer
```

### Understanding the updates to `SearchboxContainer`

The code above imports the `searchAirports` function and the `airportIndex` prop. With this the `SearchBoxContainer` can now handle parallel searching, and it uses a `Promise.all` to search both **Mapbox Search Box API** and the airport index simultaneously for better performance. It merges the results, combining airport results (first) with Mapbox results (second) in a single array which is passed to the `suggestions` state.

When a result is clicked the `retrieveSuggestion()` function now detects whether a clicked result is an airport or a Mapbox result and handles each appropriately, creating a consistent format for airport results to match the structure expected by the map.

### Add visual distinction with icons

To help users distinguish between airport results and standard search results, you'll introduce icons in the `searchSuggestion` component.

First, download the icon assets and place them in `src/assets/`:

[Download search icons](https://docs.mapbox.com/help/ja/help/img/custom-data-with-search-js/icons.zip)

Extract the zip and place `airport.svg` and `marker.svg` in your `src/assets/` folder.

Add the icon imports to your `SearchSuggestion` component:

```jsx
import React from "react"
// highlight-start
import airportUrl from "./assets/airport.svg"
import markerUrl from "./assets/marker.svg"
// highlight-end

const SearchSuggestion = ({suggestion, setSelectedResult}) => {
  return (
    <div 
      className="flex flex-col hover:bg-gray-200 hover:cursor-pointer px-3 py-2 border-b border-gray-200 last:border-b-0"
      onClick={() => setSelectedResult(suggestion)}
    >
      <div className="flex items-center">
        // highlight-start
        <img 
          className="size-4 mr-1"
          src={suggestion.feature_type === 'airport' ? airportUrl : markerUrl} 
          alt="Feature Icon" 
        />
        // highlight-end
        <div className="font-bold text-sm">{suggestion.name}</div>
      </div>
      <div className="text-[12px] text-gray-600">{suggestion.place_formatted}</div>
    </div>
  )
}

export default SearchSuggestion
```

Reload your app and test the integrated search! Try searching for:

-   **Airport codes**: Type "JFK", "LAX", "PBG" or "AKP" to see custom airport results with airplane icons
-   **Cities**: Type "Boston" or "New York" to see Mapbox results with location icons
-   **Addresses**: Type a street address to see standard search results

Notice how airport results appear first in the list, making them easy to find for aviation-focused workflows. When you click any result—whether it's a custom airport or a Mapbox result—the map flies to that location and adds a marker.

## Final product

You should now have a fully functional application that searches across 20,000+ US airports alongside standard Mapbox search results.

Below is a working demo of the final application, followed by final code snippets for the main files in the app.

> **Related content (related): [Browser the Final Source Code](https://github.com/mapbox/tutorials/tree/main/custom-data-with-search-js/)**
> 
> You can browse the completed source code for this demo, or pull down a copy to run the application locally from our tutorials repository.

## Next steps

🎉 **Congratulations!** You've successfully built a custom search experience that integrates your own data with **Mapbox Search JS**.

### What we've covered

Throughout this tutorial, you've completed the following:

-   Created a React application using the npm create @mapbox/web-app command
-   Imported custom airport data & created a hash map of the data for efficient indexing
-   Written your own search function to return results for Airports
-   Implement a custom Search Box component architecture using [`SearchBoxCore`](https://docs.mapbox.com/mapbox-search-js/api/core/search/#searchboxcore) and [`SearchSession`](https://docs.mapbox.com/mapbox-search-js/api/core/search_session/) to power the search and **TailWind CSS** to style the Search box and search suggestions
-   Handle search suggestion clicks with the [`SearchSession.retrieve`](https://docs.mapbox.com/mapbox-search-js/api/core/search_session/#searchsession#retrieve) and the [`flyTo`](https://docs.mapbox.com/mapbox-gl-js/api/map/#map#flyto) method.

### Potential enhancements

Now that you have a working foundation, consider these improvements:

**Expand search capabilities**

-   Add fuzzy name matching for airport names (not just IATA codes)
-   Search by city or region to find all nearby airports
-   Include airport type filters (international, regional, private)
-   Add search history or favorite airports

**Improve performance**

-   Implement virtual scrolling for large result sets
-   Add result caching to avoid redundant searches
-   Use Web Workers for heavy indexing operations
-   Lazy load airport data only when needed

**Enhance UX**

-   Add keyboard navigation (arrow keys, Enter, Escape)
-   Show "no results" state with helpful suggestions
-   Add loading indicators during search
-   Implement recent searches

### Related learning

Ready to learn more about implementing search functionality with Mapbox? Explore these related resources:

> **Related content (guide): [Mapbox Search JS documentation](https://docs.mapbox.com/mapbox-search-js/)**
> 
> Explore Mapbox Search JS including the core, web and react packages as well as example snippets to get started.

> **Related content (tutorial): [Local Search with Mapbox Search JS](https://docs.mapbox.com/help/ja/help/tutorials/local-search-search-box-api/)**
> 
> Learn how to build a local search app focused on a specific area or region.

> **Related content (tutorial): [POI search in a React App](https://docs.mapbox.com/help/ja/help/tutorials/poi-search-react)**
> 
> Learn how to implement category & POI searches in a React app using Mapbox Search Box API.

> **Related content (playground): [Search Box API Playground - Suggest / Retrieve](https://docs.mapbox.com/playground/search-box/suggest-retrieve)**
> 
> Test the parameters and visualize the request & response of the Search Box API's suggest and retrieve endpoint in it's playground.