diff --git a/examples/geocoder/README.md b/examples/geocoder/README.md
new file mode 100644
index 00000000..217b9558
--- /dev/null
+++ b/examples/geocoder/README.md
@@ -0,0 +1,12 @@
+# Example: Geocoder
+
+This app reproduces Mapbox's [Add a geocoder](https://docs.mapbox.com/mapbox-gl-js/example/mapbox-gl-geocoder/) example.
+
+## Usage
+
+To run this example, you need a [Mapbox token](http://visgl.github.io/react-map-gl/docs/get-started/mapbox-tokens). You can either set it as `MAPBOX_TOKEN` in `src/app.js`, or set a `MapboxAccessToken` environment variable in the command line.
+
+```bash
+npm i
+npm run start
+```
diff --git a/examples/geocoder/index.html b/examples/geocoder/index.html
new file mode 100644
index 00000000..1bff7202
--- /dev/null
+++ b/examples/geocoder/index.html
@@ -0,0 +1,41 @@
+
+
+
+
+ react-map-gl Example
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/geocoder/package.json b/examples/geocoder/package.json
new file mode 100644
index 00000000..27c44775
--- /dev/null
+++ b/examples/geocoder/package.json
@@ -0,0 +1,24 @@
+{
+ "scripts": {
+ "start": "webpack-dev-server --progress --hot --open",
+ "start-local": "webpack-dev-server --env local --progress --hot --open"
+ },
+ "dependencies": {
+ "@mapbox/mapbox-gl-geocoder": "^4.7.4",
+ "react": "^17.0.0",
+ "react-dom": "^17.0.0",
+ "react-map-gl": "^7.0.0-alpha.2",
+ "mapbox-gl": "^2.0.0"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.0.0",
+ "@babel/preset-env": "^7.0.0",
+ "@babel/preset-react": "^7.0.0",
+ "babel-loader": "^8.0.0",
+ "ts-loader": "^9.0.0",
+ "typescript": "^4.0.0",
+ "webpack": "^5.65.0",
+ "webpack-cli": "^4.9.0",
+ "webpack-dev-server": "^4.7.0"
+ }
+}
diff --git a/examples/geocoder/src/app.tsx b/examples/geocoder/src/app.tsx
new file mode 100644
index 00000000..b1b668f4
--- /dev/null
+++ b/examples/geocoder/src/app.tsx
@@ -0,0 +1,31 @@
+import * as React from 'react';
+import {render} from 'react-dom';
+import Map from 'react-map-gl';
+
+import GeocoderControl from './geocoder-control';
+import ControlPanel from './control-panel';
+
+const TOKEN = process.env.MapboxAccessToken; // Set your mapbox token here
+
+export default function App() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export function renderToDom(container) {
+ render( , container);
+}
diff --git a/examples/geocoder/src/control-panel.tsx b/examples/geocoder/src/control-panel.tsx
new file mode 100644
index 00000000..83fbceb4
--- /dev/null
+++ b/examples/geocoder/src/control-panel.tsx
@@ -0,0 +1,19 @@
+import * as React from 'react';
+
+function ControlPanel(props) {
+ return (
+
+ );
+}
+
+export default React.memo(ControlPanel);
diff --git a/examples/geocoder/src/geocoder-control.tsx b/examples/geocoder/src/geocoder-control.tsx
new file mode 100644
index 00000000..d846c8c3
--- /dev/null
+++ b/examples/geocoder/src/geocoder-control.tsx
@@ -0,0 +1,142 @@
+import * as React from 'react';
+import {useState} from 'react';
+import {useControl, Marker, ControlPosition} from 'react-map-gl';
+import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
+
+type GeocoderControlProps = {
+ mapboxAccessToken: string;
+ origin?: string;
+ zoom?: number;
+ flyTo?: boolean | object;
+ placeholder?: string;
+ proximity?: {
+ longitude: number;
+ latitude: number;
+ };
+ trackProximity?: boolean;
+ collapsed?: boolean;
+ clearAndBlurOnEsc?: boolean;
+ clearOnBlur?: boolean;
+ box?: [number, number, number, number];
+ countries?: string;
+ types?: string;
+ minLength?: number;
+ limit?: number;
+ language?: string;
+ filter?: (feature: object) => boolean;
+ localGeocoder?: Function;
+ externalGeocoder?: Function;
+ reverseMode?: 'distance' | 'score';
+ reverseGeocode?: boolean;
+ enableEventLogging?: boolean;
+ marker?: boolean | object;
+ render?: (feature: object) => string;
+ getItemValue?: (feature: object) => string;
+ mode?: 'mapbox.places' | 'mapbox.places-permanent';
+ localGeocoderOnly?: boolean;
+ autocomplete?: boolean;
+ fuzzyMatch?: boolean;
+ routing?: boolean;
+ worldview?: string;
+
+ position: ControlPosition;
+
+ onLoading?: (e: object) => void;
+ onResults?: (e: object) => void;
+ onResult?: (e: object) => void;
+ onError?: (e: object) => void;
+};
+
+export default function GeocoderControl(props: GeocoderControlProps) {
+ const [marker, setMarker] = useState(null);
+
+ const geocoder: any = useControl(
+ () => {
+ const ctrl = new MapboxGeocoder({
+ ...props,
+ accessToken: props.mapboxAccessToken
+ });
+ ctrl.on('loading', props.onLoading);
+ ctrl.on('results', props.onResults);
+ ctrl.on('result', evt => {
+ props.onResult(evt);
+
+ const {result} = evt;
+ const location =
+ result &&
+ (result.center || (result.geometry?.type === 'Point' && result.geometry.coordinates));
+ if (location) {
+ setMarker( );
+ } else {
+ setMarker(null);
+ }
+ });
+ ctrl.on('error', props.onError);
+ return ctrl;
+ },
+ {
+ position: props.position
+ }
+ );
+
+ if (geocoder._map) {
+ if (geocoder.getProximity() !== props.proximity && props.proximity !== undefined) {
+ geocoder.setProximity(props.proximity);
+ }
+ if (geocoder.getRenderFunction() !== props.render && props.render !== undefined) {
+ geocoder.setRenderFunction(props.render);
+ }
+ if (geocoder.getLanguage() !== props.language && props.language !== undefined) {
+ geocoder.setLanguage(props.language);
+ }
+ if (geocoder.getZoom() !== props.zoom && props.zoom !== undefined) {
+ geocoder.setZoom(props.zoom);
+ }
+ if (geocoder.getFlyTo() !== props.flyTo && props.flyTo !== undefined) {
+ geocoder.setFlyTo(props.zoom);
+ }
+ if (geocoder.getPlaceholder() !== props.placeholder && props.placeholder !== undefined) {
+ geocoder.setPlaceholder(props.zoom);
+ }
+ if (geocoder.getCountries() !== props.countries && props.countries !== undefined) {
+ geocoder.setCountries(props.zoom);
+ }
+ if (geocoder.getTypes() !== props.types && props.types !== undefined) {
+ geocoder.setTypes(props.zoom);
+ }
+ if (geocoder.getMinLength() !== props.minLength && props.minLength !== undefined) {
+ geocoder.setMinLength(props.zoom);
+ }
+ if (geocoder.getLimit() !== props.limit && props.limit !== undefined) {
+ geocoder.setLimit(props.zoom);
+ }
+ if (geocoder.getFilter() !== props.filter && props.filter !== undefined) {
+ geocoder.setFilter(props.zoom);
+ }
+ if (geocoder.getOrigin() !== props.origin && props.origin !== undefined) {
+ geocoder.setOrigin(props.zoom);
+ }
+ if (geocoder.getAutocomplete() !== props.autocomplete && props.autocomplete !== undefined) {
+ geocoder.setAutocomplete(props.zoom);
+ }
+ if (geocoder.getFuzzyMatch() !== props.fuzzyMatch && props.fuzzyMatch !== undefined) {
+ geocoder.setFuzzyMatch(props.zoom);
+ }
+ if (geocoder.getRouting() !== props.routing && props.routing !== undefined) {
+ geocoder.setRouting(props.zoom);
+ }
+ if (geocoder.getWorldview() !== props.worldview && props.worldview !== undefined) {
+ geocoder.setWorldview(props.zoom);
+ }
+ }
+ return marker;
+}
+
+function noop() {}
+
+GeocoderControl.defaultProps = {
+ onLoading: noop,
+ onResults: noop,
+ onResult: noop,
+ onError: noop
+};
diff --git a/examples/geocoder/tsconfig.json b/examples/geocoder/tsconfig.json
new file mode 100644
index 00000000..ff49de04
--- /dev/null
+++ b/examples/geocoder/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "target": "es2020",
+ "jsx": "react",
+ "allowSyntheticDefaultImports": true,
+ "resolveJsonModule": true,
+ "moduleResolution": "node",
+ "sourceMap": true
+ }
+}
\ No newline at end of file
diff --git a/examples/geocoder/webpack.config.js b/examples/geocoder/webpack.config.js
new file mode 100644
index 00000000..ed4df0d5
--- /dev/null
+++ b/examples/geocoder/webpack.config.js
@@ -0,0 +1,54 @@
+// NOTE: To use this example standalone (e.g. outside of repo)
+// delete the local development overrides at the bottom of this file
+
+// avoid destructuring for older Node version support
+const resolve = require('path').resolve;
+const webpack = require('webpack');
+
+const config = {
+ mode: 'development',
+
+ devServer: {
+ static: '.'
+ },
+
+ entry: {
+ app: resolve('./src/app')
+ },
+
+ output: {
+ library: 'App'
+ },
+
+ resolve: {
+ extensions: ['.ts', '.tsx', '.js', '.json']
+ },
+
+ module: {
+ rules: [
+ {
+ test: /\.(ts|js)x?$/,
+ include: [resolve('.')],
+ exclude: [/node_modules/],
+ use: [
+ {
+ loader: 'babel-loader',
+ options: {
+ presets: ['@babel/env', '@babel/react']
+ }
+ },
+ {
+ loader: 'ts-loader'
+ }
+ ]
+ }
+ ]
+ },
+
+ // Optional: Enables reading mapbox token from environment variable
+ plugins: [new webpack.EnvironmentPlugin({MapboxAccessToken: ''})]
+};
+
+// Enables bundling against src in this repo rather than the installed version
+module.exports = env =>
+ env && env.local ? require('../webpack.config.local')(config)(env) : config;