kinetic-slider
Version:
A WebGL-powered kinetic slider component using PIXI.js
246 lines (243 loc) • 8.96 kB
JavaScript
import { useRef, useCallback, useEffect } from 'react';
// Development environment check
const isDevelopment = "production" === 'development';
/**
* Hook to set up navigation controls for the slider
* Fully optimized with:
* - Batch event listener registration
* - Comprehensive error handling
* - Memory leak prevention
* - Stable handler references
* - Strong cancellation mechanisms
* - Server-side rendering safety
*/
const useNavigation = ({ onNext, onPrev, enableKeyboardNav = true, resourceManager }) => {
// Track component mount state
const isMountedRef = useRef(true);
// Track batch operations
const batchOperationsRef = useRef({
pendingListeners: new Map(),
processedCount: 0
});
// Define stable handler interface with ref
const handlersRef = useRef({
keyDownHandler: (event) => {
try {
if (!isMountedRef.current || !enableKeyboardNav)
return;
const keyEvent = event;
const handlers = handlersRef.current;
// Handle keyboard navigation
switch (keyEvent.key) {
case 'ArrowLeft':
if (handlers.latestPrevFn) {
handlers.latestPrevFn();
}
break;
case 'ArrowRight':
if (handlers.latestNextFn) {
handlers.latestNextFn();
}
break;
}
}
catch (error) {
}
},
slideChangeHandler: (event) => {
try {
if (!isMountedRef.current)
return;
const customEvent = event;
const handlers = handlersRef.current;
// Handle custom slide change events
if (customEvent.detail && typeof customEvent.detail.nextIndex === 'number') {
if (handlers.latestNextFn) {
handlers.latestNextFn();
}
}
}
catch (error) {
}
},
latestNextFn: null,
latestPrevFn: null
});
/**
* Process any pending event listeners in batch
*/
const processPendingListeners = useCallback(() => {
try {
const { pendingListeners } = batchOperationsRef.current;
// Skip if no ResourceManager or no pending listeners
if (!resourceManager || pendingListeners.size === 0)
return;
let totalProcessed = 0;
// Process each event target
pendingListeners.forEach((listenerMap, target) => {
try {
resourceManager.addEventListenerBatch(target, listenerMap);
totalProcessed += Array.from(listenerMap.values())
.reduce((sum, callbacks) => sum + callbacks.length, 0);
}
catch (error) {
if (isDevelopment) ;
}
});
// Clear the pending map
pendingListeners.clear();
// Update processed count
batchOperationsRef.current.processedCount += totalProcessed;
if (isDevelopment) ;
}
catch (error) {
// Clear pending listeners even on error
batchOperationsRef.current.pendingListeners.clear();
}
}, [resourceManager]);
/**
* Add a listener to the pending batch
*/
const addListenerToBatch = useCallback((target, eventType, callback) => {
try {
const { pendingListeners } = batchOperationsRef.current;
// Get or create map for this target
if (!pendingListeners.has(target)) {
pendingListeners.set(target, new Map());
}
const targetMap = pendingListeners.get(target);
// Get or create array for this event type
if (!targetMap.has(eventType)) {
targetMap.set(eventType, []);
}
// Add callback to the list
targetMap.get(eventType).push(callback);
return true;
}
catch (error) {
return false;
}
}, []);
// Keep the latest function references updated
useEffect(() => {
handlersRef.current.latestNextFn = onNext;
handlersRef.current.latestPrevFn = onPrev;
}, [onNext, onPrev]);
// Set up keyboard navigation
useEffect(() => {
// Skip during server-side rendering
if (typeof window === 'undefined')
return;
// Skip if keyboard navigation is disabled
if (!enableKeyboardNav)
return;
// Reset mounted state
isMountedRef.current = true;
try {
const { keyDownHandler } = handlersRef.current;
// Start performance timer if in development
const startTime = isDevelopment ? performance.now() : 0;
// Register event listener with batch processing if ResourceManager available
if (resourceManager) {
// Add to batch operations
addListenerToBatch(window, 'keydown', keyDownHandler);
// Process in batch
processPendingListeners();
}
else {
// Direct registration
window.addEventListener('keydown', keyDownHandler);
}
// Log performance if in development
if (isDevelopment && startTime > 0) ;
// Cleanup on unmount
return () => {
// Update mounted state immediately
isMountedRef.current = false;
try {
// ResourceManager handles its own cleanup
if (!resourceManager) {
window.removeEventListener('keydown', keyDownHandler);
}
}
catch (cleanupError) {
if (isDevelopment) ;
}
};
}
catch (error) {
// Return empty cleanup function
return () => { };
}
}, [enableKeyboardNav, resourceManager, addListenerToBatch, processPendingListeners]);
// Listen for custom slide change events
useEffect(() => {
// Skip during server-side rendering
if (typeof window === 'undefined')
return;
try {
const { slideChangeHandler } = handlersRef.current;
// Start performance timer if in development
const startTime = isDevelopment ? performance.now() : 0;
// Register with batch processing if ResourceManager is available
if (resourceManager) {
// Add to batch operations
addListenerToBatch(window, 'slideChange', slideChangeHandler);
// Process in batch
processPendingListeners();
}
else {
// Direct registration
window.addEventListener('slideChange', slideChangeHandler);
}
// Log performance if in development
if (isDevelopment && startTime > 0) ;
// Cleanup on unmount
return () => {
try {
// ResourceManager handles its own cleanup
if (!resourceManager) {
window.removeEventListener('slideChange', slideChangeHandler);
}
}
catch (cleanupError) {
if (isDevelopment) ;
}
};
}
catch (error) {
// Return empty cleanup function
return () => { };
}
}, [resourceManager, addListenerToBatch, processPendingListeners]);
// Expose memoized navigation methods
const goNext = useCallback(() => {
try {
if (isMountedRef.current && handlersRef.current.latestNextFn) {
const startTime = isDevelopment ? performance.now() : 0;
handlersRef.current.latestNextFn();
if (isDevelopment && startTime > 0) ;
}
}
catch (error) {
}
}, []);
const goPrev = useCallback(() => {
try {
if (isMountedRef.current && handlersRef.current.latestPrevFn) {
const startTime = isDevelopment ? performance.now() : 0;
handlersRef.current.latestPrevFn();
if (isDevelopment && startTime > 0) ;
}
}
catch (error) {
}
}, []);
return {
goNext,
goPrev,
isKeyboardEnabled: enableKeyboardNav
};
};
export { useNavigation as default };
//# sourceMappingURL=useNavigation.js.map