@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
JavaScript
/**
* @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,
};
}