@jimjam.dev/url-state
Version:
A url state manager for nextjs
207 lines • 8.15 kB
JavaScript
'use client';
import { useCallback, useState, useEffect, useMemo, useRef } from 'react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { serializeUrl, deserializeUrl } from './serializer';
/**
* URL state hook - maintains original API with performance optimizations
* Generic implementation that works with any backend
*/
export function useUrlState(uniqueKey = '') {
const searchParams = useSearchParams();
const pathname = usePathname();
const router = useRouter();
// Cache for deserialized values to avoid repeated parsing
const deserializationCache = useRef(new Map());
// Memoized state extraction from URL - optimized for performance
const state = useMemo(() => {
const result = {};
// Early return if no uniqueKey to avoid unnecessary iteration
if (!uniqueKey) {
return result;
}
// Only process params that start with uniqueKey - more efficient iteration
const relevantParams = Array.from(searchParams.entries()).filter(([key]) => key.startsWith(uniqueKey));
for (const [key, value] of relevantParams) {
const originalKey = key.slice(uniqueKey.length);
// Use cache for better performance
if (deserializationCache.current.has(value)) {
result[originalKey] = deserializationCache.current.get(value);
}
else {
try {
const deserializedValue = deserializeUrl(value);
result[originalKey] = deserializedValue;
// Cache the result (with size limit) - only cache non-empty values
if (deserializationCache.current.size < 100 && value !== '') {
deserializationCache.current.set(value, deserializedValue);
}
}
catch (error) {
console.warn(`Failed to deserialize ${originalKey}:`, error);
result[originalKey] = value;
}
}
}
return result;
}, [searchParams, uniqueKey]);
// Optimized parameter removal check
const shouldRemoveParam = useCallback((value) => {
if (value === '' || value == null)
return true;
if (Array.isArray(value) && value.length === 0)
return true;
if (typeof value === 'object' && value !== null && Object.keys(value).length === 0)
return true;
if (typeof value === 'boolean' && value === false)
return true;
return false;
}, []);
// Optimized URL update function - reduce URLSearchParams construction
const updateUrl = useCallback((updates) => {
// Early return if no updates
if (Object.keys(updates).length === 0) {
return;
}
const params = new URLSearchParams(searchParams);
let hasChanges = false;
// Batch parameter updates for better performance
Object.entries(updates).forEach(([key, value]) => {
const paramKey = uniqueKey + key;
const currentValue = params.get(paramKey);
if (shouldRemoveParam(value)) {
if (currentValue !== null) {
params.delete(paramKey);
hasChanges = true;
}
}
else {
const serializedValue = serializeUrl(value);
if (currentValue !== serializedValue) {
params.set(paramKey, serializedValue);
hasChanges = true;
}
}
});
// Only update router if there are actual changes
if (hasChanges) {
const newSearch = params.toString();
const newUrl = newSearch ? `${pathname}?${newSearch}` : pathname;
router.push(newUrl, { scroll: false });
}
}, [pathname, router, searchParams, uniqueKey, shouldRemoveParam]);
// Original API methods - keeping exact same interface
const setItem = useCallback((key, value) => {
updateUrl({ [key]: value });
}, [updateUrl]);
const setItems = useCallback((items) => {
updateUrl(items);
}, [updateUrl]);
const deleteItem = useCallback((key) => {
updateUrl({ [key]: undefined });
}, [updateUrl]);
const deleteItems = useCallback((keys) => {
const updates = {};
keys.forEach(key => {
updates[key] = undefined;
});
updateUrl(updates);
}, [updateUrl]);
const deleteAllItems = useCallback(() => {
const updates = {};
Object.keys(state).forEach(key => {
updates[key] = undefined;
});
updateUrl(updates);
}, [state, updateUrl]);
// Return exact same API as original
return {
state,
setItem,
setItems,
deleteItem,
deleteItems,
deleteAllItems,
};
}
/**
* Performance hook for batch updates and optimistic UI
* For high-frequency updates like forms with rapid changes
*/
export function useUrlStateBatch(uniqueKey = '', batchDelayMs = 100) {
const [pendingUpdates, setPendingUpdates] = useState({});
const batchTimer = useRef(undefined);
const { state: urlState, setItems } = useUrlState(uniqueKey);
// Merged state (URL state + pending updates for optimistic UI)
const state = useMemo(() => ({
...urlState,
...pendingUpdates
}), [urlState, pendingUpdates]);
const flushPendingUpdates = useCallback(() => {
if (Object.keys(pendingUpdates).length > 0) {
setItems(pendingUpdates);
setPendingUpdates({});
}
}, [pendingUpdates, setItems]);
const batchSetItem = useCallback((key, value) => {
setPendingUpdates(prev => ({ ...prev, [key]: value }));
if (batchTimer.current) {
clearTimeout(batchTimer.current);
}
batchTimer.current = setTimeout(flushPendingUpdates, batchDelayMs);
}, [flushPendingUpdates, batchDelayMs]);
const batchSetItems = useCallback((items) => {
setPendingUpdates(prev => ({ ...prev, ...items }));
if (batchTimer.current) {
clearTimeout(batchTimer.current);
}
batchTimer.current = setTimeout(flushPendingUpdates, batchDelayMs);
}, [flushPendingUpdates, batchDelayMs]);
const batchDeleteItem = useCallback((key) => {
setPendingUpdates(prev => ({ ...prev, [key]: undefined }));
if (batchTimer.current) {
clearTimeout(batchTimer.current);
}
batchTimer.current = setTimeout(flushPendingUpdates, batchDelayMs);
}, [flushPendingUpdates, batchDelayMs]);
const batchDeleteItems = useCallback((keys) => {
const updates = {};
keys.forEach(key => {
updates[key] = undefined;
});
setPendingUpdates(prev => ({ ...prev, ...updates }));
if (batchTimer.current) {
clearTimeout(batchTimer.current);
}
batchTimer.current = setTimeout(flushPendingUpdates, batchDelayMs);
}, [flushPendingUpdates, batchDelayMs]);
const batchDeleteAllItems = useCallback(() => {
const updates = {};
Object.keys(state).forEach(key => {
updates[key] = undefined;
});
setPendingUpdates(prev => ({ ...prev, ...updates }));
if (batchTimer.current) {
clearTimeout(batchTimer.current);
}
batchTimer.current = setTimeout(flushPendingUpdates, batchDelayMs);
}, [state, flushPendingUpdates, batchDelayMs]);
// Cleanup
useEffect(() => {
return () => {
if (batchTimer.current) {
clearTimeout(batchTimer.current);
}
};
}, []);
return {
state,
setItem: batchSetItem,
setItems: batchSetItems,
deleteItem: batchDeleteItem,
deleteItems: batchDeleteItems,
deleteAllItems: batchDeleteAllItems,
flushPendingUpdates,
hasPendingUpdates: Object.keys(pendingUpdates).length > 0
};
}
//# sourceMappingURL=client.js.map