The Riverwood Properties project required something that does not exist as a WordPress plugin: an interactive property map where buyers can filter listings by location, lot size, property type, and other criteria, with the map updating in real time as filters change. I built it as a React application embedded in a ClassicPress/WordPress site. Here is the architecture.

Why Not a Plugin?
There are MapBox plugins for WordPress. None of them are built for property search. They are designed to put a map on a page with some markers — useful for a contact page, not useful for a property listing search tool with dynamic filtering, custom marker styles, and property-detail panels.
Building on top of a general-purpose MapBox plugin means fighting its abstractions. Building a React application that consumes MapBox GL JS directly is more work upfront and infinitely more flexible.
Architecture
The application has three layers:
1. Data layer (WordPress REST API). A custom REST endpoint in the WordPress/ClassicPress theme exposes property listings as JSON: location coordinates, metadata (size, type, price range), and a thumbnail image. The endpoint is public and cached at the server level — MapBox initialisation fetches all listings once, and the client filters them without additional requests.
2. Map layer (MapBox GL JS). I use MapBox GL JS directly, not a React wrapper. The map instance lives in a useRef — it is a mutable external object that React does not need to re-render around. Map events (click on a marker, zoom change) dispatch to React state.
3. Filter and UI layer (React). Filter controls are React components. Filter state lives in a lightweight store. When filter state changes, the application calls MapBox’s setFilter() on the property markers layer — the map updates without a fetch.
The key performance decision: all filtering happens client-side. The full property dataset is fetched once on load. For a property inventory of a few hundred listings, this is faster than debounced server requests for every filter change.
Embedding in WordPress/ClassicPress
The React application builds to a single JS bundle. In the WordPress theme, I enqueue it via wp_enqueue_script() and output a placeholder div via a shortcode. The bundle mounts to that div.
This approach works in both WordPress and ClassicPress — no Gutenberg dependency, no block registration required. It also means the application is portable: it could embed in any CMS that can output a div and load a script.
One ClassicPress-specific note: ClassicPress does not have the block editor, so I did not have to worry about the block editor’s script loading order or conflicts with React’s own version.
MapBox GL JS Fundamentals
For property search, the critical MapBox features are:
- GeoJSON source — load all property locations as a GeoJSON
FeatureCollectionwith properties (size, type, etc.) as feature properties - Symbol layer — custom marker icons with click events
- Filter expressions —
map.setFilter('property-markers', ['all', ['>=', ['get', 'size'], minSize], ...])updates which markers are visible without modifying the source data - Popup — a
mapboxgl.Popupinstance, repositioned and repopulated on marker click
The filter expression approach is the reason client-side filtering is fast. MapBox GL is running in a WebGL context — applying a filter is a GPU operation, not a DOM traversal.
Lessons
GeoJSON filtering is fast; DOM filtering is slow. Keep all filtering in MapBox’s layer filter, not in React state that re-renders markers.
One REST call, all data. For small-to-medium property inventories, fetching all listings once is better than paginated requests. Add server-side filtering only if the inventory grows to thousands of listings.
The map instance is not React state. useRef for the map, useState for UI. Mixing them causes re-render conflicts.
ClassicPress is a viable production environment. For a site that does not need the block editor, ClassicPress is leaner and more stable than WordPress with Gutenberg. This project ran on ClassicPress without any compromises.