@ai-growth/nextjs
Version:
Seamlessly integrate Sanity CMS with Next.js applications for automated blog routing and rendering
145 lines (144 loc) • 6.85 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/**
* @fileoverview Lazy Loading React Components
*
* This module provides React components for implementing lazy loading patterns
* with JSX support, including LazyLoadComponent and error boundaries.
*/
import React, { Suspense, Component, useState, useEffect, lazy } from 'react';
import { useIntersectionObserver, useDynamicImport, } from '../utils/lazy-loading';
import { ErrorBoundary } from './ErrorBoundary';
/**
* Lazy loading component with intersection observer
*/
export const LazyLoadComponent = ({ component: Component, props = {}, fallback = null, errorFallback = null, className, style, intersectionOptions, immediate = false, }) => {
const [elementRef, isIntersecting] = useIntersectionObserver(intersectionOptions);
const shouldLoad = immediate || isIntersecting;
const { component, loading, error } = useDynamicImport(() => Promise.resolve({ default: Component }), shouldLoad);
if (error && errorFallback) {
return _jsx("div", { ref: elementRef, className: className, style: style, children: errorFallback });
}
if (loading || !component) {
return _jsx("div", { ref: elementRef, className: className, style: style, children: fallback });
}
const LoadedComponent = component;
return (_jsx("div", { ref: elementRef, className: className, style: style, children: _jsx(LoadedComponent, { ...props }) }));
};
/**
* Enhanced Suspense wrapper with error boundary
*/
export const SuspenseWrapper = ({ children, fallback = null, errorFallback, onError = () => { }, }) => {
return (_jsx(ErrorBoundary, { fallback: errorFallback ? React.createElement(errorFallback, {
error: new Error('Component error'),
retry: () => window.location.reload()
}) : null, onError: onError, children: _jsx(Suspense, { fallback: fallback, children: children }) }));
};
/**
* Error boundary specifically for lazy-loaded components
*/
export class LazyErrorBoundary extends Component {
constructor(props) {
super(props);
this.handleRetry = () => {
this.setState({ hasError: false, error: null });
};
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Lazy component loading error:', error, errorInfo);
this.props.onError?.(error);
}
render() {
if (this.state.hasError && this.state.error) {
const { ErrorFallback } = this.props;
return _jsx(ErrorFallback, { error: this.state.error, retry: this.handleRetry });
}
return this.props.children;
}
}
/**
* Create a lazy-loaded higher-order component
*/
export function withLazyLoading(loader, options = {}) {
const { fallback = null, errorFallback = null, onError = () => { }, preloadOnHover = false, hoverDelay = 300, } = options;
const LazyComponent = React.lazy(loader);
return React.forwardRef((props, ref) => {
const [isHovered, setIsHovered] = useState(false);
const [shouldPreload, setShouldPreload] = useState(false);
// Handle hover preloading
useEffect(() => {
if (preloadOnHover && isHovered && !shouldPreload) {
const timer = setTimeout(() => {
setShouldPreload(true);
}, hoverDelay);
return () => clearTimeout(timer);
}
return () => { };
}, [isHovered, preloadOnHover, hoverDelay, shouldPreload]);
const handleMouseEnter = preloadOnHover ? () => setIsHovered(true) : undefined;
const handleMouseLeave = preloadOnHover ? () => setIsHovered(false) : undefined;
return (_jsx("div", { onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, style: { display: 'contents' }, ref: ref, children: _jsx(ErrorBoundary, { fallback: errorFallback, onError: onError, children: _jsxs(Suspense, { fallback: fallback, children: [shouldPreload && _jsx(LazyComponent, { ...props, ref: ref }), !shouldPreload && _jsx(LazyComponent, { ...props, ref: ref })] }) }) }));
});
}
// ============================================================================
// LAZY LOADING UTILITIES
// ============================================================================
/**
* Create lazy component with Suspense wrapper
*/
export function createLazyComponentWithSuspense(loader, fallback = null) {
const LazyComponent = lazy(loader);
return function SuspenseWrapper(props) {
return (_jsx(React.Suspense, { fallback: fallback, children: _jsx(LazyComponent, { ...props }) }));
};
}
/**
* Route-based lazy component loader
*/
export const RouteBasedLazy = ({ routeMap, currentRoute, componentProps = {}, fallback = null, errorFallback, }) => {
const Component = routeMap[currentRoute];
if (!Component) {
if (errorFallback) {
const ErrorComponent = errorFallback;
return (_jsx(ErrorComponent, { error: new Error(`Route not found: ${currentRoute}`), retry: () => window.location.reload() }));
}
return _jsxs("div", { children: ["Route not found: ", currentRoute] });
}
const suspenseProps = {
fallback,
};
if (errorFallback) {
suspenseProps.errorFallback = errorFallback;
}
return (_jsx(SuspenseWrapper, { ...suspenseProps, children: _jsx(Component, { ...componentProps }) }));
};
/**
* Wrapper component that only renders children when in viewport
*/
export const IntersectionWrapper = ({ children, placeholder = null, options, className, style, onIntersect, }) => {
const [elementRef, isIntersecting] = useIntersectionObserver(options);
React.useEffect(() => {
if (isIntersecting && onIntersect) {
onIntersect();
}
}, [isIntersecting, onIntersect]);
return (_jsx("div", { ref: (node) => {
if (elementRef.current !== node) {
elementRef.current = node;
}
}, className: className, style: style, children: isIntersecting ? children : placeholder }));
};
export const LazyImage = ({ src, alt, placeholder = 'blur', className, ...props }) => {
const imgRef = React.useRef(null);
const [isLoaded, setIsLoaded] = React.useState(false);
React.useEffect(() => {
const img = imgRef.current;
if (img && img.complete) {
setIsLoaded(true);
}
}, []);
return (_jsxs("div", { className: "image-container", children: [!isLoaded && placeholder, _jsx("img", { ref: imgRef, src: src, alt: alt, className: className, loading: "lazy", decoding: "async", style: { display: isLoaded ? 'block' : 'none' }, onLoad: () => setIsLoaded(true), ...props })] }));
};