@wherabouts/react
v0.2.1
Published
React hooks for the Wherabouts location API
Readme
@wherabouts/react
React hooks for the Wherabouts location API — address autocomplete, reverse geocoding, and geofence detection with built-in debouncing, cancellation, and loading/error state.
Requirements
| Peer dependency | Version |
|--------------------|----------|
| react | >= 18 |
| @wherabouts/sdk | >= 0.4.1 |
Installation
# npm
npm install @wherabouts/react @wherabouts/sdk
# pnpm
pnpm add @wherabouts/react @wherabouts/sdk
# yarn
yarn add @wherabouts/react @wherabouts/sdkQuick start
All hooks accept a WheraboutsClient instance as their first argument. Create one with your publishable API key from the Wherabouts dashboard.
import { createWheraboutsClient } from "@wherabouts/sdk";
const client = createWheraboutsClient({
apiKey: "pk_live_...", // publishable key — safe in browser
});Use a publishable key (
pk_live_…) in browser code. Secret keys must stay server-side only.
Keep a stable client reference. Create the client once — at module scope or with
useMemo— and reuse it. The hooks listclientin their effect dependencies, so passing a freshly-created client on every render restarts the request (and debounce) each render.const client = useMemo(() => createWheraboutsClient({ apiKey }), [apiKey]);
Hooks
useAutocomplete
Type-ahead address search with automatic debouncing and in-flight request cancellation.
import { useAutocomplete } from "@wherabouts/react";
function AddressSearch({ client }) {
const { query, setQuery, results, loading, error } = useAutocomplete(client, {
debounceMs: 300, // default — waits 300ms after last keystroke
limit: 5, // max results to return
country: "AU", // optional ISO 3166-1 alpha-2 country filter
state: "QLD", // optional state/region filter
});
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Start typing an address..."
/>
{loading && <p>Searching…</p>}
{error && <p>Error: {error.message}</p>}
<ul>
{results.map((r) => (
<li key={r.id}>{r.formattedAddress}</li>
))}
</ul>
</div>
);
}Signature
function useAutocomplete(
client: WheraboutsClient,
options?: UseAutocompleteOptions
): UseAutocompleteResultUseAutocompleteOptions
| Option | Type | Default | Description |
|--------------------|---------------------------------|---------|------------------------------------------------------|
| debounceMs | number | 300 | Milliseconds to wait after last keystroke |
| minLength | number | 2 | Minimum trimmed query length before a request fires (the API requires q ≥ 2) |
| limit | number | — | Maximum number of suggestions to return |
| country | string | — | ISO 3166-1 alpha-2 country code filter (e.g. "AU") |
| state | string | — | State or region name filter |
| lat / lng | number | — | Bias results toward a coordinate (proximity) |
| sessionToken | string | — | Groups a run of keystrokes into one billable search (see newSessionToken()) |
| keepPreviousData | boolean | false | Keep previous results visible while the next search loads (avoids dropdown flicker) |
| cache | { storage; ttlMs?: number } | — | Opt-in client cache, e.g. { storage: sessionStorage, ttlMs: 60_000 } |
UseAutocompleteResult
| Field | Type | Description |
|---------------|------------------------|---------------------------------------------------|
| query | string | Current search string |
| setQuery | (q: string) => void | Stable setter — safe to pass directly to onChange |
| results | AddressSuggestion[] | Matching address suggestions |
| status | "idle" \| "loading" \| "success" \| "empty" \| "error" | Coarse state machine for rendering |
| loading | boolean | true while a request is in flight |
| rateLimited | boolean | true when the last request was rejected with HTTP 429 |
| error | Error \| null | Last error, or null |
| reset | () => void | Clear the query, results, and any in-flight request |
Behaviour notes:
- Queries shorter than
minLength(default 2) never fire a request. - Empty or whitespace-only query immediately clears results without firing a request.
- Each new keystroke cancels the previous pending request via
AbortController. setQueryis memoised withuseCallback— it will not cause unnecessary re-renders.
useCombobox
Headless WAI-ARIA combobox
helpers for an accessible autocomplete dropdown — keyboard navigation
(↑/↓/Home/End/Enter/Esc, with wrapping) and ARIA wiring. Bring your own markup;
pair it with useAutocomplete.
import { useAutocomplete, useCombobox } from "@wherabouts/react";
function AddressCombobox({ client }) {
const { query, setQuery, results } = useAutocomplete(client);
const { getInputProps, getListboxProps, getItemProps, activeIndex } =
useCombobox({
id: "address",
count: results.length,
onSelect: (i) => setQuery(results[i]?.formattedAddress ?? ""),
});
return (
<>
<input
{...getInputProps()}
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<ul {...getListboxProps()}>
{results.map((r, i) => (
<li key={r.id} {...getItemProps(i)} data-active={i === activeIndex}>
{r.formattedAddress}
</li>
))}
</ul>
</>
);
}getInputProps() / getListboxProps() / getItemProps(index) return the
role, aria-*, and event handlers for each element. The pure building blocks
(comboboxReducer, keyToAction, buildInputProps/buildListboxProps/buildItemProps)
are also exported for advanced use.
useReverseGeocode
Resolves a latitude/longitude coordinate to the nearest address. Pass null to reset.
import { useReverseGeocode } from "@wherabouts/react";
function NearestAddress({ client, userLocation }) {
// userLocation: { lat: number; lng: number } | null
const { address, distance, loading, error } = useReverseGeocode(client, userLocation);
if (loading) return <p>Locating…</p>;
if (error) return <p>Error: {error.message}</p>;
if (!address) return <p>No location set</p>;
return (
<div>
<p>{address.formattedAddress}</p>
<p>{distance != null ? `${distance.toFixed(0)}m away` : ""}</p>
</div>
);
}Signature
function useReverseGeocode(
client: WheraboutsClient,
coords: LatLng | null
): UseReverseGeocodeResultLatLng
interface LatLng {
lat: number;
lng: number;
}UseReverseGeocodeResult
| Field | Type | Description |
|------------|-------------------------------|----------------------------------------------|
| address | ReverseGeocodeAddress \| null | Nearest address, or null if not yet resolved |
| distance | number \| null | Distance in metres from coords to address |
| loading | boolean | true while a request is in flight |
| error | Error \| null | Last error, or null |
Behaviour notes:
- Passing
nullascoordsimmediately resetsaddressanddistancetonullwithout firing a request. - The effect re-runs only when
coords.latorcoords.lngchanges — passing a new object reference with the same values does not re-fetch. - In-flight requests are cancelled on unmount or when
coordschanges.
useZoneContains
Returns all geofence zones that contain a given coordinate. Useful for entry/exit detection, location-gating, and proximity-based UI.
import { useZoneContains } from "@wherabouts/react";
function ZoneStatus({ client, userLocation }) {
const { zones, loading, error } = useZoneContains(client, userLocation);
if (loading) return <p>Checking zones…</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
{zones.length === 0 ? (
<p>Not inside any zone</p>
) : (
<ul>
{zones.map((z) => (
<li key={z.id}>{z.name}</li>
))}
</ul>
)}
</div>
);
}Signature
function useZoneContains(
client: WheraboutsClient,
coords: LatLng | null
): UseZoneContainsResultUseZoneContainsResult
| Field | Type | Description |
|-----------|-------------------|------------------------------------------|
| zones | ZoneRecord[] | Zones that contain coords |
| loading | boolean | true while a request is in flight |
| error | Error \| null | Last error, or null |
Behaviour notes:
- Passing
nullascoordsimmediately clearszoneswithout firing a request. - Same coordinate-stability semantics as
useReverseGeocode— object identity is ignored, onlylat/lngvalues matter. - In-flight requests are cancelled on unmount or when
coordschanges.
Routing hooks — useDirections / useMatrix / useIsochrone
Fetch routing results that re-run when their params change and abort the previous
request. Pass null params to stay idle. Each returns { data, loading, error }.
import { useDirections } from "@wherabouts/react";
function Route({ client }) {
const { data, loading, error } = useDirections(client, {
from: "-33.865,151.209",
to: "-33.8,151.0",
profile: "driving", // "driving" | "walking" | "cycling"
});
if (loading) return <p>Routing…</p>;
if (error) return <p>Error: {error.message}</p>;
return <p>{data ? `${data.distance_m} m, ${data.duration_s} s` : null}</p>;
}useMatrix(client, { sources, destinations, profile? } | null)— duration/distance matrix.useIsochrone(client, { origin, durationSeconds | distanceMeters, profile? } | null)— reachability polygon.
Using the browser Geolocation API
A common pattern — combine the browser's navigator.geolocation with these hooks:
import { useState, useEffect } from "react";
import { useReverseGeocode, useZoneContains, type LatLng } from "@wherabouts/react";
function LocationAwareComponent({ client }) {
const [coords, setCoords] = useState<LatLng | null>(null);
useEffect(() => {
if (!navigator.geolocation) return;
const id = navigator.geolocation.watchPosition(
(pos) => setCoords({ lat: pos.coords.latitude, lng: pos.coords.longitude }),
console.error,
{ enableHighAccuracy: true }
);
return () => navigator.geolocation.clearWatch(id);
}, []);
const { address } = useReverseGeocode(client, coords);
const { zones } = useZoneContains(client, coords);
return (
<div>
<p>Address: {address?.formattedAddress ?? "Locating…"}</p>
<p>Zones: {zones.map((z) => z.name).join(", ") || "None"}</p>
</div>
);
}TypeScript
All hooks and their option/result types are exported:
import type {
LatLng,
UseAutocompleteOptions,
UseAutocompleteResult,
UseReverseGeocodeResult,
UseZoneContainsResult,
} from "@wherabouts/react";The package ships with bundled type declarations (.d.ts and .d.cts) for both ESM and CJS consumers.
Bundle formats
| Format | Entry | Use case |
|--------|-------------------------|-----------------------|
| ESM | dist/index.js | Bundlers (Vite, Next) |
| CJS | dist/index.cjs | Node / Jest |
License
MIT © Joseph Amani
