Appearance
State Management ​
State Management Architecture ​
AcqMarketplace uses a hybrid approach combining TanStack Query for server state, React Context for global app state, and local component state for UI interactions.
TanStack Query (React Query v5) ​
Configuration ​
File: src/lib/queryClient.ts
The query client is configured with optimized defaults:
- Stale Time: 30 minutes for most queries (optimized from 5 minutes)
- Cache Time: 1 hour for better memory management
- Retry Logic: 3 attempts with exponential backoff
- Background Refetch: Enabled for fresh data
- Refetch on Mount: Disabled to use cached data immediately
Query Patterns ​
Basic Data Fetching ​
typescript
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
function useListings(filters = {}) {
return useQuery({
queryKey: ['listings', filters],
queryFn: async () => {
const { data, error } = await supabase
.from('listings')
.select('*')
.eq('status', 'active');
if (error) throw error;
return data;
},
staleTime: 30 * 60 * 1000, // 30 minutes - optimized for better performance
});
}Query Key Patterns ​
typescript
// User-specific data
['user', userId]
['user', userId, 'listings']
['user', userId, 'offers']
// Listing data
['listings']
['listings', { category, status }]
['listing', listingId]
['listing', listingId, 'offers']
// Admin data
['admin', 'users']
['admin', 'listings', { status: 'pending' }]
['admin', 'analytics', dateRange]Mutations ​
Basic Mutation Pattern ​
typescript
import { useMutation, useQueryClient } from '@tanstack/react-query';
function useCreateListing() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (listingData) => {
const { data, error } = await supabase
.from('listings')
.insert(listingData)
.select()
.single();
if (error) throw error;
return data;
},
onSuccess: (newListing) => {
// Invalidate related queries
queryClient.invalidateQueries({ queryKey: ['listings'] });
queryClient.invalidateQueries({
queryKey: ['user', newListing.seller_id, 'listings']
});
},
});
}Optimistic Updates ​
typescript
function useUpdateListing() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateListingAPI,
onMutate: async (updatedListing) => {
// Cancel ongoing queries
await queryClient.cancelQueries({
queryKey: ['listing', updatedListing.id]
});
// Get current data
const previousListing = queryClient.getQueryData([
'listing',
updatedListing.id
]);
// Optimistically update
queryClient.setQueryData(
['listing', updatedListing.id],
updatedListing
);
return { previousListing };
},
onError: (err, updatedListing, context) => {
// Rollback on error
queryClient.setQueryData(
['listing', updatedListing.id],
context.previousListing
);
},
onSettled: (data, error, updatedListing) => {
// Refetch to ensure consistency
queryClient.invalidateQueries({
queryKey: ['listing', updatedListing.id]
});
},
});
}Cache Management ​
Query Invalidation Strategies ​
typescript
// Invalidate all listings
queryClient.invalidateQueries({ queryKey: ['listings'] });
// Invalidate specific listing
queryClient.invalidateQueries({ queryKey: ['listing', listingId] });
// Invalidate by partial key match
queryClient.invalidateQueries({
queryKey: ['user', userId],
exact: false
});
// Remove from cache entirely
queryClient.removeQueries({ queryKey: ['listing', deletedListingId] });Prefetching ​
typescript
// Prefetch related data
function prefetchListingDetails(listingId) {
queryClient.prefetchQuery({
queryKey: ['listing', listingId],
queryFn: () => fetchListingDetails(listingId),
staleTime: 10 * 60 * 1000, // 10 minutes
});
}
// Prefetch on hover
<Link
to={`/listing/${listing.id}`}
onMouseEnter={() => prefetchListingDetails(listing.id)}
>
View Details
</Link>React Context for Global State ​
Authentication Context ​
File: src/contexts/AuthContext.tsx
Manages user authentication state:
typescript
interface AuthContextType {
user: User | null;
loading: boolean;
signIn: (email: string, password: string) => Promise<void>;
signUp: (email: string, password: string) => Promise<void>;
signOut: () => Promise<void>;
updateProfile: (updates: ProfileUpdates) => Promise<void>;
}Usage Pattern ​
typescript
import { useAuth } from '@/contexts/AuthContext';
function UserProfile() {
const { user, loading, updateProfile } = useAuth();
if (loading) return <LoadingSpinner />;
if (!user) return <RedirectToLogin />;
return <ProfileForm user={user} onSave={updateProfile} />;
}Translation Context ​
File: src/contexts/translation/TranslationProvider.tsx
Manages internationalization state:
typescript
interface TranslationContextType {
language: 'ro' | 'en';
setLanguage: (lang: 'ro' | 'en') => void;
t: (key: string, options?: any) => string;
}Theme Context ​
File: src/components/ThemeProvider.tsx
Manages dark/light mode:
typescript
interface ThemeContextType {
theme: 'dark' | 'light' | 'system';
setTheme: (theme: 'dark' | 'light' | 'system') => void;
}DevMode Context ​
File: src/contexts/DevModeContext.tsx
Development utilities and debugging:
typescript
interface DevModeContextType {
devMode: boolean;
toggleDevMode: () => void;
debugInfo: DebugInfo;
}Real-time State Updates ​
Global Realtime Hook ​
File: src/hooks/useGlobalRealtimeNotifications.ts
Manages real-time updates from Supabase:
typescript
export function useGlobalRealtimeNotifications() {
const { user } = useAuth();
const queryClient = useQueryClient();
useEffect(() => {
if (!user) return;
const channel = supabase
.channel('critical-notifications')
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'offers',
filter: `seller_id=eq.${user.id}`
}, (payload) => {
// Invalidate offers query
queryClient.invalidateQueries({
queryKey: ['user', user.id, 'offers']
});
// Show notification
toast.success('New offer received!');
})
.subscribe();
return () => supabase.removeChannel(channel);
}, [user, queryClient]);
}Real-time Query Updates ​
typescript
function useRealtimeListings() {
const queryClient = useQueryClient();
useEffect(() => {
const channel = supabase
.channel('listings-updates')
.on('postgres_changes', {
event: '*',
schema: 'public',
table: 'listings'
}, (payload) => {
if (payload.eventType === 'INSERT') {
// Add to cache
queryClient.setQueryData(['listings'], (old) =>
old ? [payload.new, ...old] : [payload.new]
);
} else if (payload.eventType === 'UPDATE') {
// Update in cache
queryClient.setQueryData(['listing', payload.new.id], payload.new);
queryClient.invalidateQueries({ queryKey: ['listings'] });
}
})
.subscribe();
return () => supabase.removeChannel(channel);
}, [queryClient]);
}Local Component State ​
Form State Management ​
typescript
import { useState, useCallback } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
function CreateListingForm() {
const [step, setStep] = useState(1);
const [isSubmitting, setIsSubmitting] = useState(false);
const form = useForm({
resolver: zodResolver(listingSchema),
defaultValues: {
title: '',
price: 0,
category: '',
},
});
const handleSubmit = useCallback(async (data) => {
setIsSubmitting(true);
try {
await createListing(data);
navigate('/my-listings');
} catch (error) {
toast.error('Failed to create listing');
} finally {
setIsSubmitting(false);
}
}, []);
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)}>
{/* Form fields */}
</form>
</Form>
);
}UI State Patterns ​
typescript
// Modal state
const [isModalOpen, setIsModalOpen] = useState(false);
// Loading states
const [isLoading, setIsLoading] = useState(false);
// Selection state
const [selectedItems, setSelectedItems] = useState<string[]>([]);
// Filter state
const [filters, setFilters] = useState({
category: '',
priceRange: [0, 1000000],
sortBy: 'created_at',
});State Synchronization Patterns ​
Server-Client Sync ​
typescript
// Automatic background sync
function useAutoSync() {
const { user } = useAuth();
const queryClient = useQueryClient();
useEffect(() => {
if (!user) return;
// Sync every 5 minutes
const interval = setInterval(() => {
queryClient.invalidateQueries({
queryKey: ['user', user.id]
});
}, 5 * 60 * 1000);
return () => clearInterval(interval);
}, [user, queryClient]);
}Offline State Management ​
typescript
function useOfflineSync() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
const queryClient = useQueryClient();
useEffect(() => {
function handleOnline() {
setIsOnline(true);
// Sync when back online
queryClient.invalidateQueries();
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, [queryClient]);
return isOnline;
}Cache Optimization Patterns ​
Data-Specific Cache Strategies ​
User Profile Data ​
typescript
// Profile data changes infrequently - longer cache
const { data: profile } = useQuery({
queryKey: ['profile', userId],
queryFn: fetchUserProfile,
staleTime: 30 * 60 * 1000, // 30 minutes
refetchOnMount: false, // Use cached data immediately
gcTime: 60 * 60 * 1000, // 1 hour garbage collection
});Subscription Data ​
typescript
// Subscription status changes less frequently
const { data: subscription } = useQuery({
queryKey: ['subscription', userId],
queryFn: fetchSubscription,
staleTime: 15 * 60 * 1000, // 15 minutes
refetchOnMount: false,
});Notification Settings ​
typescript
// User preferences rarely change
const { data: settings } = useQuery({
queryKey: ['notification-settings', userId],
queryFn: fetchNotificationSettings,
staleTime: 60 * 60 * 1000, // 1 hour
});Page Access Rules ​
typescript
// Access control rules change infrequently
const { data: rules } = useQuery({
queryKey: ['page-access-rules'],
queryFn: fetchPageAccessRules,
staleTime: 30 * 60 * 1000, // 30 minutes
refetchOnWindowFocus: false,
refetchOnMount: false,
});Translation Data ​
typescript
// Translations cached in memory with TTL
const TTL_MS = 60 * 60 * 1000; // 1 hour
const translationCache = new Map();Cache Performance Benefits ​
| Data Type | Cache Time | Performance Gain | Use Case |
|---|---|---|---|
| User Profile | 30 minutes | 6x fewer requests | Account settings, seller verification |
| Subscriptions | 15 minutes | 30x fewer requests | Dashboard, pricing pages |
| Notifications | 1 hour | 120x fewer requests | Settings, preferences |
| Page Access | 30 minutes | 6x fewer requests | Navigation, role-based UI |
| Translations | 1 hour | 12x fewer requests | i18n content loading |
| Blog Posts | 10 minutes | 20x fewer requests | Admin dashboard content |
Cache Invalidation Strategies ​
Smart Invalidation ​
typescript
// Only invalidate when data actually changes
function useUpdateProfile() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateProfile,
onSuccess: (updatedProfile) => {
// Update cache directly instead of invalidating
queryClient.setQueryData(
['profile', updatedProfile.id],
updatedProfile
);
// Only invalidate related queries
queryClient.invalidateQueries({
queryKey: ['user', updatedProfile.id, 'listings']
});
},
});
}Selective Invalidation ​
typescript
// Invalidate specific data types only
function invalidateUserData(userId: string) {
queryClient.invalidateQueries({
queryKey: ['user', userId],
exact: false // Invalidate all user-related queries
});
// Keep global data cached
// queryClient.invalidateQueries({ queryKey: ['listings'] }); // Don't invalidate
}Performance Optimizations ​
Query Debouncing ​
typescript
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
function SearchListings() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebouncedValue(searchTerm, 300);
const { data: listings } = useQuery({
queryKey: ['listings', 'search', debouncedSearchTerm],
queryFn: () => searchListings(debouncedSearchTerm),
enabled: debouncedSearchTerm.length > 2,
});
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search listings..."
/>
{/* Results */}
</div>
);
}Infinite Queries ​
typescript
function useInfiniteListings(filters) {
return useInfiniteQuery({
queryKey: ['listings', 'infinite', filters],
queryFn: ({ pageParam = 0 }) =>
fetchListings({ ...filters, offset: pageParam, limit: 20 }),
getNextPageParam: (lastPage, pages) =>
lastPage.length === 20 ? pages.length * 20 : undefined,
staleTime: 5 * 60 * 1000,
});
}Memory Management ​
typescript
// Clean up unused queries
function useQueryCleanup() {
const queryClient = useQueryClient();
useEffect(() => {
const cleanup = setInterval(() => {
queryClient.getQueryCache().clear();
}, 30 * 60 * 1000); // Every 30 minutes
return () => clearInterval(cleanup);
}, [queryClient]);
}Related Documentation:
- Architecture Overview - Overall system structure
- Real-time Features - Real-time implementation
- Performance Guide - Performance optimization strategies