DEV Community

Marty Roque
Marty Roque

Posted on

Building a Real Estate Search App with Nucleux: Clean Architecture Made Simple

Real estate applications are notorious for their complexity. Between property filtering, map interactions, user preferences, and real-time updates, the state management can quickly become a tangled mess. Let's explore how Nucleux transforms this complexity into clean, maintainable TypeScript code.

The Challenge: State Management Chaos

Traditional React state management for real estate apps often leads to:

  • Provider hell with nested contexts for filters, properties, favorites, and map state
  • Prop drilling through multiple component layers
  • Complex useEffect chains trying to synchronize related state
  • Performance issues from unnecessary re-renders when any state changes

Imagine trying to coordinate updates between a property list, map markers, filter sidebar, and favorites panel using traditional Redux or Context API – the boilerplate alone would be overwhelming.

The Nucleux Approach: Atomic State Architecture

Nucleux solves these problems through atomic state management and dependency injection. Let's build a real estate search app that demonstrates four key benefits:

1. Streamlined Development: No Boilerplate, Just Logic

With Nucleux, you define your state as simple TypeScript classes:

import { Store } from 'nucleux'; interface SearchFilters { minPrice: number; maxPrice: number; bedrooms: number | null; bathrooms: number | null; propertyType: 'any' | 'house' | 'apartment' | 'condo'; location: string; } interface SavedSearch { id: number; name: string; filters: SearchFilters; } class SearchStore extends Store { filters = this.atom<SearchFilters>({ minPrice: 100000, maxPrice: 1000000, bedrooms: null, bathrooms: null, propertyType: 'any', location: '' }, 'search-filters'); savedSearches = this.atom<SavedSearch[]>([], 'saved-searches'); updateFilter = <K extends keyof SearchFilters>(key: K, value: SearchFilters[K]) => { this.filters.value = { ...this.filters.value, [key]: value }; }; saveCurrentSearch = (name: string) => { const newSearch: SavedSearch = { id: Date.now(), name, filters: this.filters.value }; this.savedSearches.value = [...this.savedSearches.value, newSearch]; }; } 
Enter fullscreen mode Exit fullscreen mode

No reducers, no action creators, no dispatch functions – just straightforward methods that update state. The 'search-filters' persistence key ensures user preferences survive browser refreshes.

2. Superior Maintainability: Clean Separation of Concerns

Each store handles a specific domain, making the codebase easier to understand and modify:

interface Property { id: string; price: number; bedrooms: number; bathrooms: number; type: 'house' | 'apartment' | 'condo'; address: string; coordinates: { lat: number; lng: number; }; imageUrl: string; description: string; } interface MapBounds { north: number; south: number; east: number; west: number; } class PropertiesStore extends Store { searchStore = this.inject(SearchStore); allProperties = this.atom<Property[]>([]); loading = this.atom<boolean>(false); mapBounds = this.atom<MapBounds | null>(null); constructor() { super(); this.loadProperties(); // Auto-refresh when filters change this.watchAtom(this.searchStore.filters, () => { this.loadProperties(); }); } filteredProperties = this.deriveAtom( [this.allProperties, this.searchStore.filters, this.mapBounds], (properties: Property[], filters: SearchFilters, bounds: MapBounds | null) => { return properties .filter(property => this.matchesFilters(property, filters)) .filter(property => this.withinMapBounds(property, bounds)); } ); private loadProperties = async (): Promise<void> => { this.loading.value = true; try { const response = await fetch( "https://raw.githubusercontent.com/BrianQMclaren/real-estate/refs/heads/master/data.json" ); const data = await response.json(); this.allProperties.value = data.property || []; } catch (error) { console.error('Failed to load properties:', error); this.allProperties.value = []; } finally { this.loading.value = false; } }; private matchesFilters = (property: Property, filters: SearchFilters): boolean => { const { minPrice, maxPrice, bedrooms, bathrooms, propertyType, location } = filters; if (property.price < minPrice || property.price > maxPrice) return false; if (bedrooms && property.bedrooms !== bedrooms) return false; if (bathrooms && property.bathrooms !== bathrooms) return false; if (propertyType !== 'any' && property.type !== propertyType) return false; if (location && !property.address.toLowerCase().includes(location.toLowerCase())) return false; return true; }; private withinMapBounds = (property: Property, bounds: MapBounds | null): boolean => { if (!bounds) return true; const { lat, lng } = property.coordinates; return lat >= bounds.south && lat <= bounds.north && lng >= bounds.west && lng <= bounds.east; }; } 
Enter fullscreen mode Exit fullscreen mode

Notice how PropertiesStore automatically injects SearchStore and reacts to filter changes. The derived filteredProperties atom efficiently recalculates only when its dependencies change.

3. Optimized Performance: Surgical Re-renders

Nucleux's atomic updates ensure components only re-render when their specific data changes:

class FavoritesStore extends Store { favorites = this.atom<string[]>([], 'favorite-properties'); toggleFavorite = (propertyId: string): void => { const current = this.favorites.value; this.favorites.value = current.includes(propertyId) ? current.filter(id => id !== propertyId) : [...current, propertyId]; }; isFavorite = (propertyId: string): boolean => { return this.favorites.value.includes(propertyId); }; } 
Enter fullscreen mode Exit fullscreen mode

When a user favorites a property, only components displaying favorite status re-render – not the entire property list, map, or filter panel.

4. Built-in Persistence: Zero-Configuration Data Retention

User preferences automatically persist across sessions without additional setup:

import React from 'react'; import { useValue, useStore, useNucleux } from 'nucleux'; const FilterPanel: React.FC = () => { const searchStore = useStore(SearchStore); const filters = useValue(searchStore.filters); const handleMinPriceChange = (e: React.ChangeEvent<HTMLInputElement>) => { const value = parseInt(e.target.value) || 0; updateFilter('minPrice', value); }; const handleMaxPriceChange = (e: React.ChangeEvent<HTMLInputElement>) => { const value = parseInt(e.target.value) || 0; updateFilter('maxPrice', value); }; const handleBedroomsChange = (e: React.ChangeEvent<HTMLInputElement>) => { const value = e.target.value ? parseInt(e.target.value) : null; updateFilter('bedrooms', value); }; return ( <div className="filter-panel" style={{ padding: '20px', border: '1px solid #ccc', borderRadius: '8px' }}> <h3>Search Filters</h3> <div style={{ marginBottom: '15px' }}> <label> Min Price: $ <input type="number" value={filters.minPrice} onChange={handleMinPriceChange} style={{ marginLeft: '5px', padding: '5px', width: '120px' }} /> </label> </div> <div style={{ marginBottom: '15px' }}> <label> Max Price: $ <input type="number" value={filters.maxPrice} onChange={handleMaxPriceChange} style={{ marginLeft: '5px', padding: '5px', width: '120px' }} /> </label> </div> <div style={{ marginBottom: '15px' }}> <label> Bedrooms: <input type="number" value={filters.bedrooms || ''} onChange={handleBedroomsChange} placeholder="Any" min="1" max="10" style={{ marginLeft: '5px', padding: '5px', width: '80px' }} /> </label> </div> <div style={{ marginBottom: '15px' }}> <label> Location: <input type="text" value={filters.location} onChange={(e) => updateFilter('location', e.target.value)} placeholder="Enter city or address" style={{ marginLeft: '5px', padding: '5px', width: '200px' }} /> </label> </div> </div> ); }; const PropertyList: React.FC = () => { const properties = useValue(PropertiesStore, 'filteredProperties'); const loading = useValue(PropertiesStore, 'loading'); if (loading) return <div>Loading properties...</div>; return ( <div className="property-list"> <h3>Properties ({properties.length} found)</h3> {properties.map(property => ( <PropertyCard key={property.id} property={property} /> ))} </div> ); }; const PropertyCard: React.FC<{ property: Property }> = ({ property }) => { const { toggleFavorite, isFavorite } = useNucleux(FavoritesStore); const favorited = isFavorite(property.id); return ( <div style={{ border: '1px solid #ddd', borderRadius: '8px', padding: '15px', marginBottom: '15px', backgroundColor: '#f9f9f9' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}> <div> <h4>${property.price.toLocaleString()}</h4> <p>{property.bedrooms} bed, {property.bathrooms} bath {property.type}</p> <p>{property.address}</p> <p style={{ color: '#666', fontSize: '14px' }}>{property.description}</p> </div> <button onClick={() => toggleFavorite(property.id)} style={{ background: favorited ? '#ff4444' : '#ddd', color: favorited ? 'white' : 'black', border: 'none', padding: '5px 10px', borderRadius: '4px', cursor: 'pointer' }} > {favorited ? '♥ Favorited' : '♡ Favorite'} </button> </div> </div> ); }; 
Enter fullscreen mode Exit fullscreen mode

The Result: Code That Scales

This TypeScript architecture delivers remarkable benefits:

  • No provider hell: Import and use stores directly in any component
  • Automatic optimization: Components re-render only when their data changes
  • Intuitive debugging: State lives in predictable class instances
  • Easy testing: Mock store methods like any other class
  • Flexible persistence: User preferences survive refreshes automatically
  • Type safety: Full TypeScript support with intelligent autocomplete

Conclusion

Real estate applications don't have to be complex. With Nucleux's atomic state management and TypeScript support, you can build feature-rich property search experiences while maintaining clean, type-safe, understandable code.

The combination of streamlined development, superior maintainability, optimized performance, and built-in persistence makes Nucleux particularly powerful for data-heavy applications like real estate platforms.

Ready to simplify your next React project? Try Nucleux and experience state management the way it should be.


Want to see this in action? Check out the live demo and explore the GitHub repository.

Top comments (3)

Collapse
 
jamey_harris_45035b37aefb profile image
Jamey Harris

Nice posting! Interested in talking to you

Collapse
 
martyroque profile image
Marty Roque

Thanks @jamey_harris_45035b37aefb! Let’s connect!

Collapse
 
jamey_harris_45035b37aefb profile image
Jamey Harris

Could you send me your email? My email: jameyharris.uw@gmail.com