Last active
March 27, 2025 02:30
-
-
Save composite/491ae1c057b37f2b7fd4775368cf8eb0 to your computer and use it in GitHub Desktop.
AI Generated react hooks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use client'; | |
import { useState, useCallback, useMemo, Dispatch, SetStateAction } from 'react'; | |
export interface DataArray<R> { | |
data: R[] & { added: R[]; updated: R[]; removed: R[]; initial: R[] }; | |
length: number; | |
isMutated: boolean; | |
isInsert: boolean; | |
isUpdate: boolean; | |
isDelete: boolean; | |
save: () => DataArray<R>; | |
reset: () => DataArray<R>; | |
clear: () => DataArray<R>; | |
add: (...item: R[]) => DataArray<R>; | |
insert: (index: number, itemOrInitializer: R | (() => R)) => DataArray<R>; | |
update: (index: number, itemOrUpdater: SetStateAction<R>) => DataArray<R>; | |
remove: (...index: number[]) => DataArray<R>; | |
filter: (predicate: (item: R, index: number, array: readonly R[]) => boolean) => DataArray<R>; | |
batch: (callback: (data: R[]) => R[]) => DataArray<R>; | |
} | |
/** | |
* A hook that provides a data array with additional properties and methods to manage the data array. | |
* @param initArray The initial array of data | |
* @returns DataArray<R> with tracking capabilities | |
*/ | |
export function useDataArray<R>(initArray: R[] = []): DataArray<R> { | |
// Core state management | |
const [data, setData] = useState<R[]>(() => [...initArray]); | |
const [added, setAdded] = useState<R[]>([]); | |
const [updated, setUpdated] = useState<R[]>([]); | |
const [removed, setRemoved] = useState<R[]>([]); | |
const [initial, setInitial] = useState<R[]>(() => [...initArray]); | |
// Helper to handle negative indices | |
const normalizeIndex = useCallback( | |
(index: number): number => { | |
if (index < 0) return Math.max(0, data.length + index); | |
return Math.min(index, data.length); | |
}, | |
[data.length] | |
); | |
// Mutation tracking properties | |
const isInsert = useMemo(() => added.length > 0, [added]); | |
const isUpdate = useMemo(() => updated.length > 0, [updated]); | |
const isDelete = useMemo(() => removed.length > 0, [removed]); | |
const isMutated = useMemo(() => isInsert || isUpdate || isDelete, [isInsert, isUpdate, isDelete]); | |
// Enhance data array with tracking properties | |
const enhancedData = useMemo(() => { | |
const result = [...data] as R[] & { added: R[]; updated: R[]; removed: R[]; initial: R[] }; | |
result.added = added; | |
result.updated = updated; | |
result.removed = removed; | |
result.initial = initial; | |
return result; | |
}, [data, added, updated, removed, initial]); | |
// Create the DataArray instance | |
const dataArray = useMemo<DataArray<R>>(() => { | |
return { | |
// Read-only properties | |
data: enhancedData, | |
length: data.length, | |
isMutated, | |
isInsert, | |
isUpdate, | |
isDelete, | |
// State management methods | |
save: () => { | |
// Update initial state to current data | |
setInitial([...data]); | |
// Clear tracking states | |
setAdded([]); | |
setUpdated([]); | |
setRemoved([]); | |
return dataArray; | |
}, | |
reset: () => { | |
setData([...initial]); | |
setAdded([]); | |
setUpdated([]); | |
setRemoved([]); | |
return dataArray; | |
}, | |
clear: () => { | |
setRemoved(prev => [...prev, ...data]); | |
setData([]); | |
return dataArray; | |
}, | |
// Data modification methods | |
add: (...items: R[]) => { | |
if (items.length === 0) return dataArray; | |
setData(prev => [...prev, ...items]); | |
setAdded(prev => [...prev, ...items]); | |
return dataArray; | |
}, | |
insert: (index: number, itemOrInitializer: R | (() => R)) => { | |
const normalizedIndex = normalizeIndex(index); | |
let newItem: R; | |
if (typeof itemOrInitializer === 'function') { | |
newItem = (itemOrInitializer as () => R)(); | |
} else { | |
newItem = itemOrInitializer; | |
} | |
setData(prev => { | |
const newData = [...prev]; | |
newData.splice(normalizedIndex, 0, newItem); | |
return newData; | |
}); | |
setAdded(prev => [...prev, newItem]); | |
return dataArray; | |
}, | |
update: (index: number, itemOrUpdater: SetStateAction<R>) => { | |
const normalizedIndex = normalizeIndex(index); | |
if (normalizedIndex >= 0 && normalizedIndex < data.length) { | |
const currentItem = data[normalizedIndex]; | |
let newItem: R; | |
if (typeof itemOrUpdater === 'function') { | |
newItem = (itemOrUpdater as (prevState: R) => R)(currentItem); | |
} else { | |
newItem = itemOrUpdater; | |
} | |
setData(prev => { | |
const newData = [...prev]; | |
newData[normalizedIndex] = newItem; | |
return newData; | |
}); | |
setUpdated(prev => [...prev, newItem]); | |
} | |
return dataArray; | |
}, | |
filter: (predicate: (item: R, index: number, array: readonly R[]) => boolean) => { | |
const filteredItems: R[] = []; | |
const excludedItems: R[] = []; | |
data.forEach((item, index, array) => { | |
if (predicate(item, index, array)) { | |
filteredItems.push(item); | |
} else { | |
excludedItems.push(item); | |
} | |
}); | |
setData(filteredItems); | |
if (excludedItems.length > 0) { | |
setRemoved(prev => [...prev, ...excludedItems]); | |
} | |
return dataArray; | |
}, | |
remove: (...indices: number[]) => { | |
if (indices.length === 0) return dataArray; | |
const normalizedIndices = indices | |
.map(normalizeIndex) | |
.filter(idx => idx >= 0 && idx < data.length) | |
.sort((a, b) => b - a); | |
if (normalizedIndices.length === 0) return dataArray; | |
const itemsToRemove = normalizedIndices.map(idx => data[idx]); | |
setData(prev => { | |
const newData = [...prev]; | |
for (const idx of normalizedIndices) { | |
newData.splice(idx, 1); | |
} | |
return newData; | |
}); | |
setRemoved(prev => [...prev, ...itemsToRemove]); | |
return dataArray; | |
}, | |
batch: (callback: (data: R[]) => R[]) => { | |
try { | |
const currentData = [...data]; | |
const newData = callback(currentData); | |
if (!Array.isArray(newData)) { | |
console.error('Batch callback must return an array'); | |
return dataArray; | |
} | |
setRemoved(prev => [...prev, ...data]); | |
setAdded(prev => [...prev, ...newData]); | |
setData(newData); | |
} catch (error) { | |
console.error('Error in batch operation:', error); | |
} | |
return dataArray; | |
}, | |
}; | |
}, [data, added, updated, removed, initial, normalizeIndex, enhancedData, isMutated, isInsert, isUpdate, isDelete]); | |
return dataArray; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use client'; | |
import { useCallback, useEffect, useRef } from 'react'; | |
// Global state for singleton animation frame | |
interface DurationCallbackState { | |
startTimestamp: number; | |
nextTimestamp: number; | |
callbacks: Array<{ | |
callback: () => void; | |
deps: unknown[]; | |
once: boolean; | |
executed: boolean; | |
}>; | |
} | |
// Singleton map to track all durations and their callbacks | |
const durationMap = new Map<number, DurationCallbackState>(); | |
let animationFrameId: number | null = null; | |
// Animation frame handler function | |
function handleAnimationFrame(timestamp: number) { | |
let shouldContinue = false; | |
// Check each duration entry | |
durationMap.forEach((state, duration) => { | |
// Calculate if it's time to execute callbacks | |
if (timestamp >= state.nextTimestamp) { | |
// Calculate next execution time | |
state.nextTimestamp = timestamp + duration; | |
// Execute all callbacks for this duration | |
state.callbacks = state.callbacks.filter((entry) => { | |
// Skip if it's a one-time callback that's already been executed | |
if (entry.once && entry.executed) { | |
return false; | |
} | |
// Execute the callback | |
entry.callback(); | |
// Mark as executed if it's a one-time callback | |
if (entry.once) { | |
entry.executed = true; | |
return false; | |
} | |
return true; | |
}); | |
// Remove this duration entry if there are no more callbacks | |
if (state.callbacks.length === 0) { | |
durationMap.delete(duration); | |
} else { | |
shouldContinue = true; | |
} | |
} else { | |
shouldContinue = true; | |
} | |
}); | |
// Continue the animation frame loop if there are still callbacks | |
if (shouldContinue) { | |
animationFrameId = requestAnimationFrame(handleAnimationFrame); | |
} else { | |
animationFrameId = null; | |
} | |
} | |
/** | |
* The effect will fire every `duration` milliseconds. | |
* @param {number} duration in milliseconds | |
* @param {(...args: T) => void} effect callback | |
* @param {T} dependencies array of dependencies; you must pass an array, even if it's empty | |
* @param {boolean} once if true, fire effects only once | |
* @returns {number} next firing date as timestamp in milliseconds | |
* @typeParam T | |
*/ | |
export function useDurationEffect<T extends unknown[]>( | |
duration: number, | |
effect: (...args: T) => void, | |
dependencies: T, | |
once: boolean = false | |
): number { | |
// Store the callback with a ref to avoid recreation on rerenders | |
const callbackRef = useRef<(...args: T) => void>(effect); | |
// Update the callback ref when the effect changes | |
useEffect(() => { | |
callbackRef.current = effect; | |
}, [effect]); | |
// Memoize the callback wrapper to maintain dependency identity | |
const callbackWrapper = useCallback(() => { | |
callbackRef.current(...dependencies); | |
// eslint-disable-next-line react-hooks/exhaustive-deps | |
}, dependencies); | |
// Setup and cleanup effect | |
useEffect(() => { | |
const now = performance.now(); | |
// Get or create the state for this duration | |
if (!durationMap.has(duration)) { | |
durationMap.set(duration, { | |
startTimestamp: now, | |
nextTimestamp: now + duration, | |
callbacks: [], | |
}); | |
} | |
const state = durationMap.get(duration)!; | |
// Add this callback to the list | |
const callbackEntry = { | |
callback: callbackWrapper, | |
deps: dependencies, | |
once, | |
executed: false, | |
}; | |
state.callbacks.push(callbackEntry); | |
// Start the animation frame if not already running | |
if (animationFrameId === null) { | |
animationFrameId = requestAnimationFrame(handleAnimationFrame); | |
} | |
// Cleanup function to remove this callback when the component unmounts | |
return () => { | |
if (durationMap.has(duration)) { | |
const state = durationMap.get(duration)!; | |
state.callbacks = state.callbacks.filter((entry) => entry.callback !== callbackWrapper); | |
if (state.callbacks.length === 0) { | |
durationMap.delete(duration); | |
// If no more durations, cancel the animation frame | |
if (durationMap.size === 0 && animationFrameId !== null) { | |
cancelAnimationFrame(animationFrameId); | |
animationFrameId = null; | |
} | |
} | |
} | |
}; | |
// eslint-disable-next-line react-hooks/exhaustive-deps | |
}, [duration, callbackWrapper, ...dependencies, once]); | |
// Return the next firing timestamp | |
return durationMap.has(duration) ? durationMap.get(duration)!.nextTimestamp : performance.now() + duration; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use client'; | |
import { useEffect, useSyncExternalStore } from 'react'; | |
/** | |
* Tailwind breakpoint switches | |
*/ | |
const BREAKPOINTS = [ | |
['sm', 'sm:block', 'sm:hidden'], | |
['md', 'md:block', 'md:hidden'], | |
['lg', 'lg:block', 'lg:hidden'], | |
['xl', 'xl:block', 'xl:hidden'], | |
['2xl', '2xl:block', '2xl:hidden'], | |
] as const; | |
type Breakpoint = (typeof BREAKPOINTS)[number][0]; | |
// Singleton to manage DOM elements and state | |
const BreakpointManager = (() => { | |
let isInitialized = false; | |
const subscribers = new Set<() => void>(); | |
/** | |
* Inspired in https://stackoverflow.com/a/70754537 | |
*/ | |
const initializeBreakpoints = () => { | |
if (isInitialized || typeof document === 'undefined') return; | |
try { | |
if (document.getElementById('tailwind-breakpoints')) { | |
isInitialized = true; | |
return; | |
} | |
const container = document.createElement('div'); | |
container.id = 'tailwind-breakpoints'; | |
// Generate HTML for each breakpoint | |
BREAKPOINTS.forEach(([name], index) => { | |
const div = document.createElement('div'); | |
div.id = `breakpoint-${name}`; | |
// Add hidden class as base | |
div.classList.add('hidden', 'w-0', 'h-0'); | |
// Add responsive classes | |
BREAKPOINTS.forEach(([_, activeClass, inactiveClass], bpIndex) => { | |
div.classList.add(index === bpIndex ? activeClass : inactiveClass); | |
}); | |
container.appendChild(div); | |
}); | |
document.body.appendChild(container); | |
isInitialized = true; | |
} catch (error) { | |
console.error('Error initializing breakpoint elements:', error); | |
} | |
}; | |
const getCurrentBreakpoint = (): Breakpoint | undefined => { | |
if (typeof document === 'undefined') return undefined; | |
try { | |
for (const [name] of BREAKPOINTS) { | |
const element = document.getElementById(`breakpoint-${name}`); | |
if (element && element.offsetParent !== null) { | |
return name; | |
} | |
} | |
} catch (error) { | |
console.error('Error getting current breakpoint:', error); | |
} | |
return undefined; | |
}; | |
const subscribe = (callback: () => void) => { | |
if (typeof window === 'undefined') return () => {}; | |
subscribers.add(callback); | |
const handleResize = () => { | |
subscribers.forEach((cb) => cb()); | |
}; | |
window.addEventListener('resize', handleResize); | |
return () => { | |
subscribers.delete(callback); | |
if (subscribers.size === 0) { | |
window.removeEventListener('resize', handleResize); | |
} | |
}; | |
}; | |
const getSnapshot = () => { | |
return getCurrentBreakpoint(); | |
}; | |
return { | |
initializeBreakpoints, | |
getCurrentBreakpoint, | |
subscribe, | |
getSnapshot, | |
}; | |
})(); | |
/** | |
* React hook that returns the current Tailwind breakpoint | |
* @returns The current active breakpoint (sm, md, lg, xl, 2xl) or undefined | |
* | |
* @example | |
* ```tsx | |
* const breakpoint = useTailwindBreakpoint(); | |
* | |
* return ( | |
* <div> | |
* Current breakpoint: {breakpoint ?? 'xs'} | |
* </div> | |
* ); | |
* ``` | |
*/ | |
export function useTailwindBreakpoint() { | |
useEffect(() => { | |
BreakpointManager.initializeBreakpoints(); | |
}, []); | |
const breakpoint = useSyncExternalStore( | |
BreakpointManager.subscribe, | |
BreakpointManager.getSnapshot, | |
() => undefined // Server snapshot | |
); | |
return breakpoint; | |
} | |
export const breakpoints = BREAKPOINTS.map(([key]) => key); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment