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]; }; }
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; }; }
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); }; }
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> ); };
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)
Nice posting! Interested in talking to you
Thanks @jamey_harris_45035b37aefb! Let’s connect!
Could you send me your email? My email: jameyharris.uw@gmail.com