Skip to content
⚛️ intermediate 20 minutes

React Integration Guide

Build a property search interface in React with custom hooks, state management, and reusable components powered by BayutAPI.

What you'll build: A React property search UI with filters and listing cards
Prerequisites: React 18+ · Node.js 18+ · RapidAPI account (free) · Basic React knowledge

Prerequisites

  • React 18+ project (Create React App, Vite, or similar)
  • A RapidAPI account with a BayutAPI subscription (get your free API key)
  • Familiarity with React hooks and async patterns

Installation

Create a new React project if you don’t have one:

npm create vite@latest my-property-app -- --template react
cd my-property-app
npm install

No additional API client libraries are needed — the native fetch API works perfectly.

Authentication Setup

Create a configuration file for your API credentials. Never expose your API key in client-side code in production — use a backend proxy instead:

// src/config/api.js
export const API_CONFIG = {
  baseUrl: "https://bayut14.p.rapidapi.com",
  headers: {
    "x-rapidapi-key": import.meta.env.VITE_RAPIDAPI_KEY,
    "x-rapidapi-host": "bayut14.p.rapidapi.com",
  },
};

Add your key to .env:

VITE_RAPIDAPI_KEY=your_api_key_here

Basic Example: Property Search Hook

Create a custom hook that handles property searching, loading states, and pagination:

// src/hooks/usePropertySearch.js
import { useState, useCallback } from "react";
import { API_CONFIG } from "../config/api";

export function usePropertySearch() {
  const [properties, setProperties] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [pagination, setPagination] = useState({ page: 1, totalPages: 0, total: 0 });

  const searchProperties = useCallback(async (filters = {}) => {
    setLoading(true);
    setError(null);

    try {
      const params = new URLSearchParams({
        purpose: filters.purpose || "for-sale",
        page: (filters.page || 1).toString(),
        ...(filters.locationId && { location_ids: filters.locationId }),
        ...(filters.minPrice && { price_min: filters.minPrice }),
        ...(filters.maxPrice && { price_max: filters.maxPrice }),
        ...(filters.bedrooms && { bedrooms_min: filters.bedrooms, bedrooms_max: filters.bedrooms }),
      });

      const response = await fetch(
        `${API_CONFIG.baseUrl}/search-property?${params}`,
        { headers: API_CONFIG.headers }
      );

      if (!response.ok) throw new Error(`API error: ${response.status}`);

      const data = await response.json();

      setProperties(data.data.properties);
      setPagination({
        page: data.data.page,
        totalPages: data.data.totalPages,
        total: data.data.total,
      });
    } catch (err) {
      setError(err.message);
      setProperties([]);
    } finally {
      setLoading(false);
    }
  }, []);

  return { properties, loading, error, pagination, searchProperties };
}

Location Autocomplete Hook

Build a hook for location typeahead search:

// src/hooks/useAutocomplete.js
import { useState, useCallback } from "react";
import { API_CONFIG } from "../config/api";

export function useAutocomplete() {
  const [suggestions, setSuggestions] = useState([]);
  const [loading, setLoading] = useState(false);

  const search = useCallback(async (query) => {
    if (!query || query.length < 2) {
      setSuggestions([]);
      return;
    }

    setLoading(true);

    try {
      const params = new URLSearchParams({ query, langs: "en" });
      const response = await fetch(
        `${API_CONFIG.baseUrl}/autocomplete?${params}`,
        { headers: API_CONFIG.headers }
      );

      if (!response.ok) throw new Error("Autocomplete failed");

      const data = await response.json();
      setSuggestions(
        data.data.locations.map((hit) => ({
          id: hit.externalID,
          name: hit.name,
          type: hit.type,
        }))
      );
    } catch {
      setSuggestions([]);
    } finally {
      setLoading(false);
    }
  }, []);

  return { suggestions, loading, search };
}

Advanced Example: Property Search Component

Put it all together in a complete search interface:

// src/components/PropertySearch.jsx
import { useState } from "react";
import { usePropertySearch } from "../hooks/usePropertySearch";
import { useAutocomplete } from "../hooks/useAutocomplete";

function PropertyCard({ property }) {
  return (
    <div style={{ border: "1px solid #333", borderRadius: 8, padding: 16, marginBottom: 12 }}>
      <h3>{property.title.en}</h3>
      <p style={{ fontSize: 20, fontWeight: "bold" }}>
        AED {property.price?.toLocaleString()}
      </p>
      <p>
        {property.bedrooms} bed · {property.bathrooms} bath · {property.area} sqft
      </p>
      <p style={{ color: "#888" }}>{property.location}</p>
    </div>
  );
}

export default function PropertySearch() {
  const [locationQuery, setLocationQuery] = useState("");
  const [selectedLocation, setSelectedLocation] = useState(null);
  const { suggestions, search: searchLocations } = useAutocomplete();
  const { properties, loading, error, pagination, searchProperties } = usePropertySearch();

  const handleLocationInput = (e) => {
    const value = e.target.value;
    setLocationQuery(value);
    searchLocations(value);
  };

  const handleSelectLocation = (location) => {
    setSelectedLocation(location);
    setLocationQuery(location.name);
    searchProperties({ locationId: location.id, purpose: "for-sale" });
  };

  const handlePageChange = (page) => {
    if (selectedLocation) {
      searchProperties({ locationId: selectedLocation.id, purpose: "for-sale", page });
    }
  };

  return (
    <div style={{ maxWidth: 800, margin: "0 auto", padding: 20 }}>
      <h1>Property Search</h1>

      <div style={{ position: "relative", marginBottom: 20 }}>
        <input
          type="text"
          value={locationQuery}
          onChange={handleLocationInput}
          placeholder="Search for a location..."
          style={{ width: "100%", padding: 12, fontSize: 16 }}
        />
        {suggestions.length > 0 && (
          <ul style={{ position: "absolute", top: "100%", left: 0, right: 0, background: "#1a1a1a", border: "1px solid #333", listStyle: "none", padding: 0, margin: 0, zIndex: 10 }}>
            {suggestions.map((loc) => (
              <li key={loc.id} onClick={() => handleSelectLocation(loc)} style={{ padding: 12, cursor: "pointer", borderBottom: "1px solid #333" }}>
                {loc.name} <span style={{ color: "#888" }}>({loc.type})</span>
              </li>
            ))}
          </ul>
        )}
      </div>

      {loading && <p>Loading properties...</p>}
      {error && <p style={{ color: "red" }}>Error: {error}</p>}

      {pagination.total > 0 && (
        <p>{pagination.total.toLocaleString()} properties found</p>
      )}

      {properties.map((prop) => (
        <PropertyCard key={prop.id} property={prop} />
      ))}

      {pagination.totalPages > 1 && (
        <div style={{ display: "flex", gap: 8, justifyContent: "center", marginTop: 20 }}>
          <button onClick={() => handlePageChange(pagination.page - 1)} disabled={pagination.page <= 1}>
            Previous
          </button>
          <span>Page {pagination.page} of {pagination.totalPages}</span>
          <button onClick={() => handlePageChange(pagination.page + 1)} disabled={pagination.page >= pagination.totalPages}>
            Next
          </button>
        </div>
      )}
    </div>
  );
}

Next Steps

Start Building with BayutAPI

Get your free API key and make your first request in under 5 minutes.

900 free requests/month — no credit card required