@ai-growth/nextjs
Version:
Seamlessly integrate Sanity CMS with Next.js applications for automated blog routing and rendering
193 lines (192 loc) • 6.37 kB
JavaScript
import { useEffect, useState, useCallback, useMemo } from 'react';
import { getDocumentsByType } from '../utils';
const listCache = new Map();
/**
* React hook for fetching lists of CMS content with pagination
*/
export function useCmsContentList(options) {
const { contentType, enabled = true, staleTime = 5 * 60 * 1000, // 5 minutes
onSuccess, onError, limit = 10, offset = 0, ...fetchOptions } = options;
const [content, setContent] = useState([]);
const [isLoading, setIsLoading] = useState(enabled);
const [isError, setIsError] = useState(false);
const [error, setError] = useState(null);
const [isFetching, setIsFetching] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [totalCount, setTotalCount] = useState();
const [currentOffset, setCurrentOffset] = useState(offset);
// Generate cache key
const cacheKey = useMemo(() => {
return `list:${contentType}:${JSON.stringify({
...fetchOptions,
limit,
offset: 0, // Always cache from beginning
})}`;
}, [contentType, fetchOptions, limit]);
// Check if cached data is stale
const isStale = useMemo(() => {
const cached = listCache.get(cacheKey);
if (!cached)
return true;
return Date.now() - cached.timestamp > cached.staleTime;
}, [cacheKey, content]);
// Transform Sanity documents to CmsContent format
const transformDocuments = useCallback((documents) => {
return documents.map(doc => ({
_id: doc._id,
_type: doc._type,
slug: doc.slug?.current || '',
title: doc.title || 'Untitled',
content: doc.content || doc.body || null,
metadata: doc.seo,
publishedAt: doc.publishedAt,
author: doc.author,
}));
}, []);
// Fetch content function
const fetchContent = useCallback(async (isLoadMore = false) => {
if (!enabled)
return;
const effectiveOffset = isLoadMore ? currentOffset : 0;
// Check cache for initial load
if (!isLoadMore && !isStale && !isFetching) {
const cached = listCache.get(cacheKey);
if (cached) {
setContent(cached.data);
setHasMore(cached.hasMore);
setTotalCount(cached.totalCount);
setIsLoading(false);
setIsError(false);
setError(null);
return;
}
}
try {
setIsFetching(true);
setIsError(false);
setError(null);
if (!isLoadMore) {
setIsLoading(true);
}
const result = await getDocumentsByType(contentType, {
...fetchOptions,
limit,
offset: effectiveOffset,
includeTotal: true,
});
const transformedContent = transformDocuments(result.documents);
const newHasMore = result.documents.length === limit;
const newTotalCount = result.total;
if (isLoadMore) {
// Append to existing content
setContent(prev => [...prev, ...transformedContent]);
setCurrentOffset(prev => prev + limit);
}
else {
// Replace content
setContent(transformedContent);
setCurrentOffset(limit);
// Cache the initial result
listCache.set(cacheKey, {
data: transformedContent,
timestamp: Date.now(),
staleTime,
hasMore: newHasMore,
totalCount: newTotalCount ?? 0,
});
}
setHasMore(newHasMore);
setTotalCount(newTotalCount);
setIsLoading(false);
onSuccess?.(isLoadMore ? content.concat(transformedContent) : transformedContent);
}
catch (err) {
const errorObj = err instanceof Error ? err : new Error(String(err));
setError(errorObj);
setIsError(true);
setIsLoading(false);
onError?.(errorObj);
}
finally {
setIsFetching(false);
}
}, [
enabled,
contentType,
fetchOptions,
limit,
currentOffset,
cacheKey,
isStale,
isFetching,
staleTime,
transformDocuments,
onSuccess,
onError,
content,
]);
// Load more function
const loadMore = useCallback(async () => {
if (!hasMore || isFetching)
return;
await fetchContent(true);
}, [hasMore, isFetching, fetchContent]);
// Refetch function for manual refresh
const refetch = useCallback(async () => {
listCache.delete(cacheKey); // Clear cache
setCurrentOffset(0);
await fetchContent(false);
}, [cacheKey, fetchContent]);
// Reset function to clear state
const reset = useCallback(() => {
setContent([]);
setIsLoading(enabled);
setIsError(false);
setError(null);
setIsFetching(false);
setHasMore(true);
setTotalCount(undefined);
setCurrentOffset(0);
}, [enabled]);
// Initial fetch effect
useEffect(() => {
if (enabled) {
fetchContent(false);
}
}, [enabled, contentType, JSON.stringify(fetchOptions)]);
return {
content,
isLoading,
isError,
error,
isFetching,
hasMore,
totalCount: totalCount ?? 0,
refetch,
loadMore,
reset,
};
}
/**
* Hook for fetching featured/recent content
*/
export function useFeaturedContent(contentType = 'post', limit = 5, options = {}) {
return useCmsContentList({
...options,
contentType,
limit,
orderBy: 'publishedAt desc',
additionalFilter: options.additionalFilter || 'featured == true',
});
}
/**
* Hook for fetching recent content
*/
export function useRecentContent(contentType = 'post', limit = 10, options = {}) {
return useCmsContentList({
...options,
contentType,
limit,
orderBy: 'publishedAt desc',
});
}