link,[object Object]
Skip to content

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 TypeCache TimePerformance GainUse Case
User Profile30 minutes6x fewer requestsAccount settings, seller verification
Subscriptions15 minutes30x fewer requestsDashboard, pricing pages
Notifications1 hour120x fewer requestsSettings, preferences
Page Access30 minutes6x fewer requestsNavigation, role-based UI
Translations1 hour12x fewer requestsi18n content loading
Blog Posts10 minutes20x fewer requestsAdmin 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: