Angular-inspired deferred loading for React - Smart, performant component loading with compound triggers, prefetching.
- Multiple Triggers: viewport, interaction, timer, idle, condition, immediate
- Compound Triggers: Combine multiple conditions (e.g., viewport + interaction)
- Smart Prefetching: Conservative, moderate, or aggressive strategies
- Zero Layout Shift: Consistent dimensions throughout loading states
- Optimized Performance: Memoized hooks, efficient re-renders
- Customizable: Bring your own styling and loading states
- TypeScript: Full type safety included
npm install @ekm1/react-whenimport { When } from '@ekm1/react-when';
import { lazy } from 'react';
const HeavyChart = lazy(() => import('./HeavyChart'));
function App() {
return (
<When
triggers="viewport"
placeholder={() => <div className="h-64 bg-gray-200 rounded">Loading chart...</div>}
loading={() => <div className="h-64 bg-gray-200 rounded animate-pulse">Almost ready...</div>}
>
<HeavyChart />
</When>
);
}Load when component enters the viewport:
// Load once when entering viewport
<When triggers="viewport-once">
<ExpensiveComponent />
</When>
// Load every time entering viewport
<When triggers="viewport">
<RefreshableComponent />
</When>
// Custom intersection options
<When
triggers="viewport"
intersectionOptions={{ threshold: 0.5, rootMargin: '100px' }}
>
<Component />
</When>Load on user interaction:
// Default events: click, mouseenter
<When triggers="interaction">
<InteractiveMap />
</When>
// Custom interaction events
<When
triggers="interaction"
interactionEvents={['click', 'focus', 'touchstart']}
>
<TouchComponent />
</When>Load after a delay:
<When
triggers="timer"
delay={3000} // 3 seconds
>
<DelayedWidget />
</When>Load when browser is idle:
<When
triggers="idle"
idleOptions={{ timeout: 5000 }}
>
<BackgroundTask />
</When>Load based on state/props:
function UserContent({ isAuthenticated }) {
return (
<When
triggers="condition"
condition={isAuthenticated}
>
<PrivateContent />
</When>
);
}Load immediately:
<When triggers="immediate">
<AlwaysLoadComponent />
</When>Combine multiple triggers for advanced loading strategies:
// Load when BOTH in viewport AND user interacts
<When triggers={['viewport', 'interaction']}>
<ExpensiveInteractiveMap />
</When>
// Load when BOTH in viewport AND after 5 seconds
<When
triggers={['viewport', 'timer']}
delay={5000}
>
<ComponentThatNeedsBoth />
</When>
// Load when idle AND authenticated
<When
triggers={['idle', 'condition']}
condition={user.isLoggedIn}
>
<UserAnalytics />
</When>Optimize loading with smart prefetching:
// Conservative: prefetch when ALL triggers are nearly satisfied
<When
triggers={['viewport', 'interaction']}
prefetchStrategy="conservative"
>
<Component />
</When>
// Moderate: prefetch when ANY trigger is satisfied
<When
triggers={['viewport', 'interaction']}
prefetchStrategy="moderate"
>
<Component />
</When>
// Aggressive: prefetch immediately
<When
triggers="viewport"
prefetchStrategy="aggressive"
>
<Component />
</When>
// None: disable prefetching
<When
triggers="interaction"
prefetchStrategy="none"
>
<Component />
</When>Prevent layout shift with properly sized loading states:
<When
triggers="viewport"
// Before triggers are met
placeholder={({ timerState, interactions }) => (
<div className="h-64 bg-gray-100 rounded flex items-center justify-center">
<div className="text-center">
<div className="animate-spin w-6 h-6 border-2 border-blue-500 rounded-full mb-2" />
<p>Waiting for viewport...</p>
{timerState && <p>Timer: {timerState.remaining}ms</p>}
</div>
</div>
)}
// While loading
loading={({ progress, prefetched }) => (
<div className="h-64 bg-gray-100 rounded flex items-center justify-center">
<div className="text-center">
<div className="animate-spin w-6 h-6 border-2 border-green-500 rounded-full mb-2" />
<p>{prefetched ? 'Using prefetched data...' : 'Loading component...'}</p>
{progress && <div className="w-32 bg-gray-300 rounded-full h-2">
<div className="bg-green-500 h-2 rounded-full" style={{width: `${progress}%`}} />
</div>}
</div>
</div>
)}
// Error handling
error={({ errorCount, retry }) => (
<div className="h-64 bg-red-100 border border-red-300 rounded flex items-center justify-center">
<div className="text-center">
<p className="text-red-800 mb-2">Failed to load ({errorCount} attempts)</p>
<button onClick={retry} className="px-4 py-2 bg-red-500 text-white rounded">
Retry
</button>
</div>
</div>
)}
>
<YourComponent />
</When>Prevent flickering with minimum loading duration:
<When
triggers="viewport"
minimumLoading={500} // Always show loading for at least 500ms
>
<FastLoadingComponent />
</When>Automatic retries on component load failures:
<When
triggers="interaction"
retryCount={3} // Retry up to 3 times
onLoadError={(error, errorInfo) => {
console.error('Component failed to load:', error);
analytics.track('component_load_error', errorInfo);
}}
>
<Component />
</When>Built-in ARIA support:
<When
triggers="viewport"
aria-label="Loading analytics dashboard"
className="focus:ring-2 focus:ring-blue-500"
>
<Dashboard />
</When>function AnalyticsDashboard() {
const [selectedTimeRange, setSelectedTimeRange] = useState('7d');
return (
<div className="space-y-6">
{/* Load immediately - critical data */}
<When triggers="immediate">
<KeyMetrics />
</When>
{/* Load when scrolled into view */}
<When triggers="viewport" prefetchStrategy="conservative">
<TrafficChart timeRange={selectedTimeRange} />
</When>
{/* Load only when user shows interest */}
<When triggers="interaction" interactionEvents={['click', 'mouseenter']}>
<DetailedAnalytics />
</When>
{/* Load in background when idle */}
<When triggers="idle">
<ReportGenerator />
</When>
</div>
);
}function ArticleReader({ article }) {
const [showComments, setShowComments] = useState(false);
return (
<article>
{/* Critical content loads immediately */}
<When triggers="immediate">
<ArticleHeader article={article} />
</When>
<When triggers="immediate">
<ArticleContent content={article.content} />
</When>
{/* Related articles load when scrolled */}
<When triggers="viewport" prefetchStrategy="moderate">
<RelatedArticles articleId={article.id} />
</When>
{/* Comments load conditionally */}
<When
triggers="condition"
condition={showComments}
minimumLoading={300}
>
<CommentSection articleId={article.id} />
</When>
<button onClick={() => setShowComments(true)}>
Load Comments
</button>
</article>
);
}| Prop | Type | Default | Description |
|---|---|---|---|
triggers |
TriggerType | TriggerType[] |
['viewport'] |
When to load the component |
delay |
number |
0 |
Delay in ms for timer trigger |
condition |
boolean |
true |
Boolean condition for condition trigger |
prefetchStrategy |
'none' | 'conservative' | 'moderate' | 'aggressive' |
'conservative' |
Prefetching strategy |
minimumLoading |
number |
0 |
Minimum loading duration in ms |
placeholder |
ReactNode | Function |
null |
Content shown before triggers are met |
loading |
ReactNode | Function |
null |
Content shown while loading |
error |
ReactNode | Function |
null |
Content shown on error |
onLoad |
() => void |
() => {} |
Called when loading starts |
onLoadError |
(error: Error) => void |
() => {} |
Called when loading fails |
intersectionOptions |
IntersectionObserverInit |
{} |
Options for viewport triggers |
idleOptions |
IdleRequestOptions |
{} |
Options for idle trigger |
interactionEvents |
string[] |
['click', 'mouseenter'] |
Events for interaction trigger |
retryCount |
number |
3 |
Number of retry attempts on error |
'viewport'- Load when entering viewport (every time)'viewport-once'- Load when entering viewport (once only)'interaction'- Load on user interaction'timer'- Load after delay'idle'- Load when browser is idle'condition'- Load when condition is true'immediate'- Load immediately
interface PlaceholderProps {
timerState?: {
expired: boolean;
progress: number;
remaining: number;
};
interactions?: {
[eventType: string]: number;
};
idleTime?: number;
}interface LoadingProps {
progress?: number;
prefetched?: boolean;
}interface ErrorProps {
errorCount: number;
retry: () => void;
}Always match dimensions between placeholder, loading, and final component:
// β Bad - causes layout shift
<When
triggers="viewport"
placeholder={() => <div>Loading...</div>}
>
<div className="h-64 w-full bg-blue-500">Large Component</div>
</When>
// β
Good - consistent dimensions
<When
triggers="viewport"
placeholder={() => <div className="h-64 w-full bg-gray-200 animate-pulse" />}
loading={() => <div className="h-64 w-full bg-gray-300 animate-pulse" />}
>
<div className="h-64 w-full bg-blue-500">Large Component</div>
</When>- Critical content:
immediate - Above-the-fold:
immediateorviewport - Below-the-fold:
viewport-once - Interactive features:
interaction - Background tasks:
idle - Conditional content:
condition
Use code splitting with React.lazy:
const HeavyComponent = lazy(() => import('./HeavyComponent'));
<When triggers="interaction">
<HeavyComponent />
</When>Always provide error states:
<When
triggers="viewport"
error={({ retry, errorCount }) => (
<div className="p-4 border border-red-300 rounded">
<p>Failed to load component (attempt {errorCount})</p>
<button onClick={retry}>Try Again</button>
</div>
)}
>
<Component />
</When>We welcome contributions! Please see our Contributing Guide for details.
git clone https://github.com/ekm1/react-when.git
cd react-when
npm install
# Run tests
npm test
# Build library
npm run build
# Run example
npm run exampleMIT Β© Migel Hoxha
Check out our interactive examples to see all features in action!
Happy deferred loading! π