UNPKG

@ai-growth/nextjs

Version:

Seamlessly integrate Sanity CMS with Next.js applications for automated blog routing and rendering

420 lines (419 loc) 13.9 kB
/** * @fileoverview Enhanced CMS Content Hooks with Advanced Caching * * This module provides React hooks for fetching CMS content with comprehensive * caching strategies, SWR patterns, and cache invalidation capabilities. */ import { useEffect, useState, useCallback, useMemo } from 'react'; import { fetchContentBySlug, fetchContentById } from '../utils'; import { defaultCacheManager, CacheKeys, CacheTags } from '../utils/cache-manager'; // ============================================================================ // ENHANCED CMS CONTENT HOOKS // ============================================================================ /** * Enhanced hook for fetching CMS content by slug with advanced caching */ export function useCmsContentCached(slug, options = {}) { const { contentType = 'post', initialData = null, enabled = true, ttl = 30 * 60 * 1000, // 30 minutes staleTime = 5 * 60 * 1000, // 5 minutes revalidateInBackground = true, refetchOnWindowFocus = false, refetchOnReconnect = true, onSuccess, onError, cacheTags = [], ...fetchOptions } = options; // State management const [content, setContent] = useState(initialData); const [isLoading, setIsLoading] = useState(!initialData && enabled && !!slug); const [isError, setIsError] = useState(false); const [error, setError] = useState(null); const [isFetching, setIsFetching] = useState(false); const [isStale, setIsStale] = useState(false); const [isRevalidating, setIsRevalidating] = useState(false); const [cacheHit, setCacheHit] = useState(false); // Generate cache key const cacheKey = useMemo(() => { if (!slug) return null; return CacheKeys.content(contentType, slug); }, [slug, contentType]); // Enhanced fetcher function const fetcher = useCallback(async () => { const data = await fetchContentBySlug(contentType, slug, fetchOptions); if (!data) { throw new Error(`Content not found for slug: ${slug}`); } return data; }, [contentType, slug, fetchOptions]); // Fetch content with SWR pattern const fetchContent = useCallback(async (isBackground = false) => { if (!slug || !enabled || !cacheKey) return; try { setIsFetching(true); if (isBackground) { setIsRevalidating(true); } else { setIsError(false); setError(null); } const result = await defaultCacheManager.getWithSWR(cacheKey, fetcher, { ttl, staleTime, tags: [CacheTags.CONTENT, contentType, ...cacheTags], revalidateInBackground, }); setContent(result.data); setIsStale(result.stale); setCacheHit(result.hit); setIsLoading(false); if (result.data) { onSuccess?.(result.data); } if (result.revalidating) { setIsRevalidating(true); } else { setIsRevalidating(false); } } catch (err) { const errorObj = err instanceof Error ? err : new Error(String(err)); setError(errorObj); setIsError(true); setContent(null); setIsLoading(false); setIsRevalidating(false); onError?.(errorObj); } finally { setIsFetching(false); } }, [ slug, enabled, cacheKey, fetcher, ttl, staleTime, contentType, cacheTags, revalidateInBackground, onSuccess, onError, ]); // Manual refetch function const refetch = useCallback(async () => { if (cacheKey) { await defaultCacheManager.delete(cacheKey); } await fetchContent(false); }, [cacheKey, fetchContent]); // Reset function const reset = useCallback(() => { setContent(initialData); setIsLoading(!initialData && enabled && !!slug); setIsError(false); setError(null); setIsFetching(false); setIsStale(false); setIsRevalidating(false); setCacheHit(false); }, [initialData, enabled, slug]); // Invalidate cache const invalidate = useCallback(async () => { if (cacheKey) { await defaultCacheManager.delete(cacheKey); } }, [cacheKey]); // Initial fetch effect useEffect(() => { if (slug && enabled) { fetchContent(false); } }, [slug, enabled, fetchContent]); // Window focus refetch useEffect(() => { if (!refetchOnWindowFocus) return; const handleFocus = () => { if (slug && enabled && isStale) { fetchContent(true); } }; window.addEventListener('focus', handleFocus); return () => window.removeEventListener('focus', handleFocus); }, [refetchOnWindowFocus, slug, enabled, isStale, fetchContent]); // Online/reconnect refetch useEffect(() => { if (!refetchOnReconnect) return; const handleOnline = () => { if (slug && enabled && isStale) { fetchContent(true); } }; window.addEventListener('online', handleOnline); return () => window.removeEventListener('online', handleOnline); }, [refetchOnReconnect, slug, enabled, isStale, fetchContent]); return { content, isLoading, isError, error, isFetching, isStale, isRevalidating, cacheHit, refetch, reset, invalidate, }; } /** * Enhanced hook for fetching CMS content by ID with caching */ export function useCmsContentByIdCached(id, options = {}) { const { initialData = null, enabled = true, ttl = 30 * 60 * 1000, staleTime = 5 * 60 * 1000, revalidateInBackground = true, onSuccess, onError, cacheTags = [], ...fetchOptions } = options; // State management const [content, setContent] = useState(initialData); const [isLoading, setIsLoading] = useState(!initialData && enabled && !!id); const [isError, setIsError] = useState(false); const [error, setError] = useState(null); const [isFetching, setIsFetching] = useState(false); const [isStale, setIsStale] = useState(false); const [isRevalidating, setIsRevalidating] = useState(false); const [cacheHit, setCacheHit] = useState(false); // Generate cache key const cacheKey = useMemo(() => { if (!id) return null; return CacheKeys.contentById(id); }, [id]); // Enhanced fetcher function const fetcher = useCallback(async () => { const data = await fetchContentById(id, fetchOptions); if (!data) { throw new Error(`Content not found for ID: ${id}`); } return data; }, [id, fetchOptions]); // Fetch content with SWR pattern const fetchContent = useCallback(async (isBackground = false) => { if (!id || !enabled || !cacheKey) return; try { setIsFetching(true); if (isBackground) { setIsRevalidating(true); } else { setIsError(false); setError(null); } const result = await defaultCacheManager.getWithSWR(cacheKey, fetcher, { ttl, staleTime, tags: [CacheTags.CONTENT, ...cacheTags], revalidateInBackground, }); setContent(result.data); setIsStale(result.stale); setCacheHit(result.hit); setIsLoading(false); if (result.data) { onSuccess?.(result.data); } if (result.revalidating) { setIsRevalidating(true); } else { setIsRevalidating(false); } } catch (err) { const errorObj = err instanceof Error ? err : new Error(String(err)); setError(errorObj); setIsError(true); setContent(null); setIsLoading(false); setIsRevalidating(false); onError?.(errorObj); } finally { setIsFetching(false); } }, [ id, enabled, cacheKey, fetcher, ttl, staleTime, cacheTags, revalidateInBackground, onSuccess, onError, ]); // Manual refetch function const refetch = useCallback(async () => { if (cacheKey) { await defaultCacheManager.delete(cacheKey); } await fetchContent(false); }, [cacheKey, fetchContent]); // Reset function const reset = useCallback(() => { setContent(initialData); setIsLoading(!initialData && enabled && !!id); setIsError(false); setError(null); setIsFetching(false); setIsStale(false); setIsRevalidating(false); setCacheHit(false); }, [initialData, enabled, id]); // Invalidate cache const invalidate = useCallback(async () => { if (cacheKey) { await defaultCacheManager.delete(cacheKey); } }, [cacheKey]); // Initial fetch effect useEffect(() => { if (id && enabled) { fetchContent(false); } }, [id, enabled, fetchContent]); return { content, isLoading, isError, error, isFetching, isStale, isRevalidating, cacheHit, refetch, reset, invalidate, }; } // ============================================================================ // CACHE INVALIDATION HOOKS // ============================================================================ /** * Hook for cache invalidation operations */ export function useCacheInvalidation() { const invalidateContent = useCallback(async (contentType, slug) => { if (slug) { const key = CacheKeys.content(contentType, slug); await defaultCacheManager.delete(key); } else { await defaultCacheManager.invalidate({ strategy: 'tag-based', tags: [contentType], }); } }, []); const invalidateContentLists = useCallback(async (contentType) => { if (contentType) { await defaultCacheManager.invalidate({ strategy: 'tag-based', tags: [CacheTags.CONTENT_LIST, contentType], }); } else { await defaultCacheManager.invalidate({ strategy: 'tag-based', tags: [CacheTags.CONTENT_LIST], }); } }, []); const invalidateByTags = useCallback(async (tags) => { await defaultCacheManager.invalidate({ strategy: 'tag-based', tags, }); }, []); const invalidateByPattern = useCallback(async (pattern) => { await defaultCacheManager.invalidate({ strategy: 'manual', pattern, }); }, []); const clearAllCache = useCallback(async () => { await defaultCacheManager.clear(); }, []); return { invalidateContent, invalidateContentLists, invalidateByTags, invalidateByPattern, clearAllCache, }; } /** * Hook for cache preloading */ export function useCachePreloading() { const preloadContent = useCallback(async (contentType, slug, fetchOptions) => { const key = CacheKeys.content(contentType, slug); const status = await defaultCacheManager.has(key); if (!status.exists || status.stale) { try { const data = await fetchContentBySlug(contentType, slug, fetchOptions); if (data) { await defaultCacheManager.set(key, data, { tags: [CacheTags.CONTENT, contentType], }); } } catch (error) { console.warn(`Failed to preload content ${contentType}:${slug}`, error); } } }, []); const preloadContentById = useCallback(async (id, fetchOptions) => { const key = CacheKeys.contentById(id); const status = await defaultCacheManager.has(key); if (!status.exists || status.stale) { try { const data = await fetchContentById(id, fetchOptions); if (data) { await defaultCacheManager.set(key, data, { tags: [CacheTags.CONTENT], }); } } catch (error) { console.warn(`Failed to preload content ID ${id}`, error); } } }, []); return { preloadContent, preloadContentById, }; } /** * Hook for cache monitoring and debugging */ export function useCacheMonitoring() { const [metrics, setMetrics] = useState(() => defaultCacheManager.getMetrics()); const refreshMetrics = useCallback(() => { setMetrics(defaultCacheManager.getMetrics()); }, []); const resetMetrics = useCallback(() => { defaultCacheManager.resetMetrics(); refreshMetrics(); }, [refreshMetrics]); // Auto-refresh metrics useEffect(() => { const interval = setInterval(refreshMetrics, 10000); // Every 10 seconds return () => clearInterval(interval); }, [refreshMetrics]); return { metrics, refreshMetrics, resetMetrics, }; }