DEV Community

Sachin Maurya
Sachin Maurya

Posted on

One Hook That Killed 23 Components: The Context-Aware API Pattern

"The best abstractions hide complexity while exposing power." This principle led us to create a hook pattern that eliminated thousands of lines of code while making our application smarter.

Picture this: You have testimonials on every page of your application. But each page needs different testimonials:

  • GenAI page: AI success stories
  • Mobile app page: App store reviews
  • GIS page: Mapping project testimonials
  • ERP page: Integration case studies

Traditional solution: Create 23 separate testimonial components.

Our solution: One hook that automatically knows what to fetch.

const { data } = useTestimonials(pagelink);
Enter fullscreen mode Exit fullscreen mode

This single line replaced 8,500 lines of duplicate code and revolutionized how we handle multi-context applications.

Table of Contents

The Multi-Context Hell {#the-problem}

Multiple business verticals through one platform. Each vertical needs the same UI components but with different data sources.

The Traditional Nightmare

// Before: Separate component for each context
const GenAiTestimonials = () => {
  const { data } = useQuery('/api/genai-testimonials', transformGenAiTestimonials);
  return <TestimonialCarousel data={data} />;
};

const MobileAppTestimonials = () => {
  const { data } = useQuery('/api/mobile-testimonials', transformMobileTestimonials);
  return <TestimonialCarousel data={data} />;
};

const GISTestimonials = () => {
  const { data } = useQuery('/api/gis-testimonials', transformGISTestimonials);
  return <TestimonialCarousel data={data} />;
};

// ... 20 more similar components
Enter fullscreen mode Exit fullscreen mode

Problems everywhere:

  • 🔴 23 duplicate testimonial components
  • 🔴 Inconsistent implementations
  • 🔴 Update one = update all
  • 🔴 Testing nightmare (23 × test cases)
  • 🔴 Bundle size explosion

The Prop Drilling Solution (Also Terrible)

// Slightly better, but still painful
const TestimonialsSection = ({ 
  apiEndpoint, 
  transformFunction, 
  cacheKey,
  retryCount,
  staleTime,
  errorMessage 
}) => {
  const { data } = useQuery({
    queryKey: [cacheKey],
    queryFn: () => fetch(apiEndpoint).then(transformFunction),
    retry: retryCount,
    staleTime
  });

  if (!data) return <div>{errorMessage}</div>;
  return <TestimonialCarousel data={data} />;
};

// Parent component nightmare
<TestimonialsSection 
  apiEndpoint="/api/genai-testimonials"
  transformFunction={transformGenAiTestimonials}
  cacheKey="genai-testimonials"
  retryCount={3}
  staleTime={300000}
  errorMessage="Failed to load GenAI testimonials"
/>
Enter fullscreen mode Exit fullscreen mode

Components shouldn't care about API endpoints, cache keys, or transformation functions!

The Breakthrough Pattern {#the-breakthrough}

What if components could automatically get the right data based on context?

// After: One component, infinite contexts
const TestimonialsSection = ({ pagelink }) => {
  const { data, isLoading, error } = useTestimonials(pagelink);

  if (isLoading) return <TestimonialSkeleton />;
  if (error) return <TestimonialError onRetry={refetch} />;

  return <TestimonialCarousel data={data} />;
};

// Usage - component automatically knows what to fetch
<TestimonialsSection pagelink="genai" />      // Gets GenAI testimonials
<TestimonialsSection pagelink="mobile-app" /> // Gets mobile testimonials
<TestimonialsSection pagelink="gis" />        // Gets GIS testimonials
Enter fullscreen mode Exit fullscreen mode

The magic happens in the hook:

const useTestimonials = createHookMap({
  'genai': useGenAiTestimonials,
  'mobile-app': useMobileAppTestimonials,
  'gis': useGISTestimonials,
  'erp': useERPTestimonials,
  'default': useGeneralTestimonials
}, 'default');
Enter fullscreen mode Exit fullscreen mode

Deep Dive: Hook Mapping Architecture {#architecture}

The Core Pattern

type HookMap<T> = Record<string, () => UseQueryResult<T>>;
type HookSelector<T> = (context?: string) => UseQueryResult<T>;

export function createHookMap<T>(
  hookMap: HookMap<T>,
  defaultKey: string
): HookSelector<T> {
  return (context?: string): UseQueryResult<T> => {
    // Smart hook selection with fallback strategy
    const selectedHook = context && hookMap[context] 
      ? hookMap[context] 
      : hookMap[defaultKey];

    if (!selectedHook) {
      console.warn(`No hook found for context: ${context}, falling back to default`);

      // Return controlled error state instead of crashing
      return {
        data: undefined,
        isLoading: false,
        isError: true,
        error: new Error(`Invalid context: ${context}`),
        refetch: () => Promise.resolve({ data: undefined } as any),
        // ... other UseQueryResult properties
      } as UseQueryResult<T>;
    }

    return selectedHook();
  };
}
Enter fullscreen mode Exit fullscreen mode

Advanced Multi-Level Context Resolution

For complex applications, we support nested contexts:

export function createNestedHookMap<T>(
  hookMap: Record<string, Record<string, () => UseQueryResult<T>>>,
  defaultPath: string
): HookSelector<T> {
  return (contextPath?: string): UseQueryResult<T> => {
    if (!contextPath) {
      return resolveDefaultHook(hookMap, defaultPath);
    }

    const [context, subContext] = contextPath.split('.');

    // Resolution priority:
    // 1. Exact match: 'genai.document-analyzer'
    // 2. Context default: 'genai.default'  
    // 3. Global default: 'default.default'

    if (hookMap[context]?.[subContext]) {
      return hookMap[context][subContext]();
    }

    if (hookMap[context]?.['default']) {
      return hookMap[context]['default']();
    }

    return resolveDefaultHook(hookMap, defaultPath);
  };
}

// Usage example
export const useProcessingMode = createNestedHookMap({
  'genai': {
    'document-analyzer': useDocumentAnalyzerModes,
    'chat-interface': useChatInterfaceModes,
    'image-processor': useImageProcessorModes,
    'default': useGenAiProcessingModes
  },
  'mobile-app': {
    'ios': useIOSProcessingModes,
    'android': useAndroidProcessingModes,
    'cross-platform': useCrossPlatformModes,
    'default': useMobileProcessingModes
  }
}, 'genai.default');

// Component usage
const { data } = useProcessingMode('genai.document-analyzer');
Enter fullscreen mode Exit fullscreen mode

Real-World Hook Library

Here's our actual production hook mapping:

// API hook library
export const useHeroSection = createHookMap({
  'home': useHomeHero,
  'genai': useGenAiHero,
  'mobile-app': useMobileAppHero,
  'gis-solutions': useGISHero,
  'erp': useERPHero,
  'about': useAboutHero,
  'contact': useContactHero,
  'services': useServicesHero
}, 'home');

export const useTestimonials = createHookMap({
  'genai': useGenAiTestimonials,
  'mobile-app': useMobileAppTestimonials,
  'gis-solutions': useGISTestimonials,
  'erp': useERPTestimonials,
  'services': useServicesTestimonials,
  'default': useGeneralTestimonials
}, 'default');

export const useFeatures = createHookMap({
  'genai': useGenAiFeatures,
  'mobile-app': useMobileAppFeatures,
  'gis-solutions': useGISFeatures,
  'erp': useERPFeatures,
  'default': useGeneralFeatures
}, 'default');

export const usePricing = createHookMap({
  'genai': useGenAiPricing,
  'mobile-app': useMobileAppPricing,
  'enterprise': useEnterprisePricing,
  'default': useStandardPricing
}, 'default');
Enter fullscreen mode Exit fullscreen mode

Advanced Patterns {#advanced-patterns}

Conditional Hook Mapping

// Dynamic hook selection based on user permissions
export const useRestrictedContent = createConditionalHookMap({
  conditions: [
    {
      when: (user) => user?.role === 'admin',
      hookMap: {
        'genai': useAdminGenAiContent,
        'mobile-app': useAdminMobileContent
      }
    },
    {
      when: (user) => user?.subscription === 'premium',
      hookMap: {
        'genai': usePremiumGenAiContent,
        'mobile-app': usePremiumMobileContent
      }
    }
  ],
  fallback: {
    'genai': usePublicGenAiContent,
    'mobile-app': usePublicMobileContent
  }
});

// Usage with automatic permission handling
const ContentSection = ({ pagelink }) => {
  const user = useCurrentUser();
  const { data } = useRestrictedContent(pagelink, user);

  return <ContentDisplay data={data} />;
};
Enter fullscreen mode Exit fullscreen mode

Hook Composition Patterns

// Combine multiple context-aware hooks
export const usePageData = (pagelink: string) => {
  const hero = useHeroSection(pagelink);
  const testimonials = useTestimonials(pagelink);
  const features = useFeatures(pagelink);
  const pricing = usePricing(pagelink);

  // Smart loading state - show content as it becomes available
  const loadingStates = [hero, testimonials, features, pricing];
  const isLoading = loadingStates.every(state => state.isLoading);
  const hasError = loadingStates.some(state => state.error);

  return {
    data: {
      hero: hero.data,
      testimonials: testimonials.data,
      features: features.data,
      pricing: pricing.data
    },
    isLoading,
    hasError,
    refetchAll: () => Promise.all(loadingStates.map(state => state.refetch))
  };
};
Enter fullscreen mode Exit fullscreen mode

Cache Strategies Per Context

// Different caching strategies for different contexts
const createContextAwareHook = (endpoint: string, transform: Function) => {
  return () => useQuery({
    queryKey: [endpoint],
    queryFn: () => fetch(`/api${endpoint}`).then(transform),

    // Context-specific caching
    staleTime: getCacheTime(endpoint),
    cacheTime: getRetentionTime(endpoint),
    refetchOnWindowFocus: shouldRefetchOnFocus(endpoint),
    retry: getRetryStrategy(endpoint)
  });
};

const getCacheTime = (endpoint: string): number => {
  const strategies = {
    '/testimonials': 10 * 60 * 1000,    // 10 minutes - changes rarely
    '/pricing': 60 * 60 * 1000,        // 1 hour - very stable
    '/user-dashboard': 30 * 1000,      // 30 seconds - personal data
    '/real-time-data': 0,              // No caching - always fresh
  };

  return strategies[endpoint] || 5 * 60 * 1000; // Default 5 minutes
};
Enter fullscreen mode Exit fullscreen mode

Type Safety Across Contexts {#type-safety}

Our pattern maintains complete type safety across all contexts:

// Shared data types
interface TestimonialData {
  id: string;
  name: string;
  role: string;
  company: string;
  quote: string;
  rating: number;
  image?: string;
  featured: boolean;
  source: string; // Context tracking
}

// Context-specific transformers with validation
export const transformGenAiTestimonials = (
  data: unknown
): TestimonialData[] => {
  // Runtime type validation
  if (!isValidTestimonialResponse(data)) {
    logger.error('Invalid GenAI testimonial data', { data });
    return getFallbackTestimonials('genai');
  }

  const apiData = data as GenAiTestimonialApiResponse;

  return apiData.data.testimonials.map(item => ({
    id: item.id.toString(),
    name: sanitizeString(item.name),
    role: sanitizeString(item.role),
    company: sanitizeString(item.company),
    quote: sanitizeString(item.quote),
    rating: Math.min(5, Math.max(0, item.rating || 5)),
    image: buildImageUrl(item.image?.url) || getDefaultAvatar(item.name),
    featured: Boolean(item.featured),
    source: 'genai'
  }));
};

// Type guard for runtime safety
function isValidTestimonialResponse(data: unknown): boolean {
  if (!data || typeof data !== 'object') return false;

  const obj = data as any;
  return (
    'data' in obj &&
    'testimonials' in obj.data &&
    Array.isArray(obj.data.testimonials) &&
    obj.data.testimonials.every((t: any) => 
      typeof t.id !== 'undefined' &&
      typeof t.name === 'string' &&
      typeof t.quote === 'string'
    )
  );
}

// Full TypeScript inference in components
const TestimonialsSection = ({ pagelink }: { pagelink: string }) => {
  const { data } = useTestimonials(pagelink);
  //    ^? TestimonialData[] | undefined

  return (
    <div>
      {data?.map(testimonial => (
        <TestimonialCard 
          key={testimonial.id}
          name={testimonial.name}     // ✅ Fully typed
          company={testimonial.company} // ✅ Fully typed
          rating={testimonial.rating}   // ✅ Fully typed
        />
      ))}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Performance Optimizations {#performance}

Intelligent Caching Strategy

// Shared cache optimization
const useSharedTestimonials = () => {
  return useQuery({
    queryKey: ['testimonials', 'shared'],
    queryFn: fetchSharedTestimonials,
    staleTime: 15 * 60 * 1000, // 15 minutes
    cacheTime: 60 * 60 * 1000, // 1 hour retention
  });
};

// Context-specific with fallback to shared
const useContextTestimonials = (pagelink: string) => {
  const sharedQuery = useSharedTestimonials();
  const contextQuery = useQuery({
    queryKey: ['testimonials', pagelink],
    queryFn: () => fetchContextTestimonials(pagelink),
    enabled: !!pagelink && pagelink !== 'default',
    staleTime: 5 * 60 * 1000,
  });

  // Return context-specific if available, otherwise shared
  if (contextQuery.data && contextQuery.data.length > 0) {
    return contextQuery;
  }

  return sharedQuery;
};
Enter fullscreen mode Exit fullscreen mode

Prefetching Strategies

// Predictive prefetching based on user navigation patterns
export const usePrefetchStrategy = (currentPagelink: string) => {
  const queryClient = useQueryClient();

  useEffect(() => {
    const prefetchTargets = getPrefetchTargets(currentPagelink);

    // Prefetch likely next pages
    prefetchTargets.forEach(target => {
      queryClient.prefetchQuery({
        queryKey: ['testimonials', target],
        queryFn: () => fetchTestimonials(target),
        staleTime: 10 * 60 * 1000
      });
    });

    // Prefetch related contexts
    const relatedContexts = getRelatedContexts(currentPagelink);
    relatedContexts.forEach(context => {
      setTimeout(() => {
        queryClient.prefetchQuery({
          queryKey: ['features', context],
          queryFn: () => fetchFeatures(context)
        });
      }, 2000); // Delayed prefetch
    });
  }, [currentPagelink, queryClient]);
};

// Smart prefetch targets based on analytics
const getPrefetchTargets = (current: string): string[] => {
  const navigationPatterns = {
    'home': ['genai', 'mobile-app', 'about'],
    'genai': ['genai-chatbot', 'about', 'contact'],
    'mobile-app': ['services', 'pricing', 'contact'],
  };

  return navigationPatterns[current] || [];
};
Enter fullscreen mode Exit fullscreen mode

Real-World Battle Stories {#battle-stories}

Case Study 1: The Testimonial Explosion

Problem: 23 testimonial components across the platform, each with subtle differences.

Before:

components/testimonials/
├── GenAiTestimonials.tsx           (245 lines)
├── MobileAppTestimonials.tsx       (267 lines)
├── GISTestimonials.tsx             (234 lines)
├── ERPTestimonials.tsx             (289 lines)
├── ServicesTestimonials.tsx        (198 lines)
... 18 more files
Total: 5,670 lines of code
Enter fullscreen mode Exit fullscreen mode

After:

components/testimonials/
├── TestimonialsSection.tsx         (89 lines)
hooks/
├── useTestimonials.ts              (34 lines)
config/
├── testimonialHooks.ts             (67 lines)
Total: 190 lines of code
Enter fullscreen mode Exit fullscreen mode

Impact: 96.6% code reduction, 100% feature parity

Case Study 2: The Feature Matrix Problem

Challenge: Different features for different product pages, with complex conditional logic.

Old approach:

const FeatureSection = ({ page, userTier, region, experiment }) => {
  const [features, setFeatures] = useState([]);

  useEffect(() => {
    let endpoint = '/api/features';

    if (page === 'genai') {
      endpoint = userTier === 'premium' 
        ? '/api/genai-premium-features'
        : '/api/genai-basic-features';
    } else if (page === 'mobile-app') {
      endpoint = region === 'US' 
        ? '/api/mobile-us-features'
        : '/api/mobile-global-features';
    }
    // ... 50 more lines of conditional logic

    fetchFeatures(endpoint).then(setFeatures);
  }, [page, userTier, region, experiment]);

  // Complex rendering logic...
};
Enter fullscreen mode Exit fullscreen mode

New approach:

const FeatureSection = ({ pagelink }) => {
  const user = useCurrentUser();
  const { data } = useFeatures(pagelink, user);

  return <FeatureGrid features={data} />;
};

// All complexity moved to hook configuration
const useFeatures = createConditionalHookMap({
  contexts: ['genai', 'mobile-app', 'gis', 'erp'],
  conditions: [
    { when: isPremiumUser, hookSuffix: '-premium' },
    { when: isUSRegion, hookSuffix: '-us' },
    { when: isExperimentB, hookSuffix: '-experiment-b' }
  ]
});
Enter fullscreen mode Exit fullscreen mode

Case Study 3: The Dashboard Data Nightmare

Scenario: User dashboard showing different widgets based on user's subscribed services.

Before: 15 different dashboard components, massive prop drilling

After: One dashboard component with context-aware widgets

const DashboardPage = ({ userId }) => {
  const user = useUserProfile(userId);
  const services = user?.subscribedServices || [];

  return (
    <Dashboard>
      {services.map(service => (
        <DashboardWidget 
          key={service}
          type="metrics" 
          context={service}
        />
      ))}
    </Dashboard>
  );
};

// Widget automatically knows what metrics to show
const DashboardWidget = ({ type, context }) => {
  const { data } = useDashboardData(`${context}.${type}`);
  return <MetricsWidget data={data} />;
};
Enter fullscreen mode Exit fullscreen mode

The Results {#results}

After implementing context-aware hooks across our entire platform:

Code Metrics

Metric Before After Improvement
Testimonial Components 23 1 -95.7%
Feature Components 18 1 -94.4%
Total Lines of Code 37,500 29,000 -22.7%
Duplicate Code 8,500 lines 0 lines -100%
Bundle Size +15% (per context) -12% (overall) -27%

Developer Experience

  • Time to add new context: 4 hours → 15 minutes (-93.8%)
  • Bug reports from data issues: 23/month → 3/month (-87%)
  • Code review time: 45 min → 8 min (-82%)
  • Developer satisfaction: "Working with APIs" went from 4.2/10 → 8.7/10

Performance Impact

  • API calls reduced: 40% fewer requests (intelligent caching)
  • Bundle size: 27% smaller (no duplicate components)
  • Memory usage: 35% less (shared component instances)
  • Cache hit rate: 78% (context-aware caching strategies)

Business Impact

  • Feature delivery velocity: 3x faster
  • Cross-team collaboration: Improved (standardized patterns)
  • Bug fix time: 60% reduction (centralized logic)
  • New developer onboarding: 2 weeks → 3 days

Advanced Use Cases

Multi-Tenant SaaS Applications

// Perfect for white-label solutions
const useBrandedContent = createHookMap({
  'client-a': useClientAContent,
  'client-b': useClientBContent,
  'client-c': useClientCContent,
  'default': useDefaultContent
}, 'default');

// Component works across all tenants
const BrandedHero = () => {
  const tenant = useTenant();
  const { data } = useBrandedContent(tenant.slug);

  return <HeroSection {...data} />;
};
Enter fullscreen mode Exit fullscreen mode

E-commerce Category Pages

// One product listing, all categories
const useProducts = createNestedHookMap({
  'electronics': {
    'phones': usePhoneProducts,
    'laptops': useLaptopProducts,
    'default': useElectronicsProducts
  },
  'fashion': {
    'men': useMensFashion,
    'women': useWomensFashion,
    'default': useFashionProducts
  }
}, 'electronics.default');

// Usage: useProducts('fashion.women')
Enter fullscreen mode Exit fullscreen mode

Content Management Systems

// Dynamic page builder
const usePageContent = createHookMap({
  'blog': useBlogContent,
  'landing': useLandingContent,
  'product': useProductContent,
  'custom': useCustomContent
}, 'custom');

const DynamicPage = ({ pageType, pageId }) => {
  const { data } = usePageContent(`${pageType}/${pageId}`);

  return <PageBuilder blocks={data.blocks} />;
};
Enter fullscreen mode Exit fullscreen mode

Best Practices We've Learned

1. Design Hooks for Composition

// Good: Composable hooks
const usePageData = (context: string) => {
  const hero = useHero(context);
  const features = useFeatures(context);
  const testimonials = useTestimonials(context);

  return { hero, features, testimonials };
};

// Bad: Monolithic hook
const useAllPageData = (context: string) => {
  // Fetches everything in one giant request
};
Enter fullscreen mode Exit fullscreen mode

2. Implement Graceful Degradation

const createResilientHookMap = <T>(hookMap: HookMap<T>, defaultKey: string) => {
  return (context?: string) => {
    try {
      const hook = hookMap[context] || hookMap[defaultKey];
      const result = hook();

      // If primary source fails, try fallback
      if (result.error && context !== defaultKey) {
        return hookMap[defaultKey]();
      }

      return result;
    } catch (error) {
      console.error('Hook execution failed:', error);
      return createErrorState<T>(error);
    }
  };
};
Enter fullscreen mode Exit fullscreen mode

3. Monitor Hook Performance

// Track hook usage and performance
const withMetrics = <T>(hookName: string, hook: () => UseQueryResult<T>) => {
  return () => {
    const startTime = performance.now();
    const result = hook();
    const endTime = performance.now();

    // Track execution time
    analytics.track('hook_performance', {
      hookName,
      executionTime: endTime - startTime,
      cacheHit: !result.isFetching && !!result.data,
      error: !!result.error
    });

    return result;
  };
};
Enter fullscreen mode Exit fullscreen mode

Conclusion

Context-aware API hooks represent a fundamental shift in how we think about data fetching in React applications. By moving context awareness into the hook layer, we've achieved:

  1. Radical Code Reduction: 95%+ elimination of duplicate components
  2. Type Safety: End-to-end TypeScript coverage
  3. Performance: Intelligent caching and prefetching
  4. Developer Experience: Simple mental model, powerful abstractions
  5. Maintainability: Centralized logic, easier testing

The pattern scales from simple use cases to complex multi-tenant applications. Start with one context-aware hook, measure the impact, then expand.

Remember: The best abstractions hide complexity while exposing power. Context-aware hooks do exactly that.


Resources

Connect with me:

Have you implemented similar patterns? What challenges did you face? Share your experience in the comments!

Top comments (0)