diff --git a/.changeset/shiny-pots-appear.md b/.changeset/shiny-pots-appear.md new file mode 100644 index 0000000..812b219 --- /dev/null +++ b/.changeset/shiny-pots-appear.md @@ -0,0 +1,5 @@ +--- +"vurtis": patch +--- + +Export useTimeout() and useVurttle() hooks. diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 44167e7..5480a71 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -3,6 +3,12 @@ export * from './useResizeObserver'; export {useIsoEffect} from './useIsoEffect'; export {useMounted} from './useMounted'; +export { + useTimeout, + type TimeoutCallback, + type TimeoutOptions, +} from './useTimeout'; + export { useWindowEvent, type WindowEventName, diff --git a/src/hooks/useTimeout.ts b/src/hooks/useTimeout.ts new file mode 100644 index 0000000..530cbc7 --- /dev/null +++ b/src/hooks/useTimeout.ts @@ -0,0 +1,38 @@ +import {useEffect, useRef} from 'react'; +import type {TimeoutId} from 'beeftools'; + +import {useIsoEffect} from './useIsoEffect'; + +export type TimeoutCallback = (timestamp: number) => void; + +export interface TimeoutOptions { + duration?: number; + disabled?: boolean; +} + +export function useTimeout( + callback: TimeoutCallback, + options: TimeoutOptions = {}, +): void { + const {duration = 0, disabled = false} = options; + + const callbackRef = useRef(); + const timeoutRef = useRef(); + + useIsoEffect(() => { + callbackRef.current = callback; + }, [callback]); + + useEffect(() => { + if (!disabled) { + timeoutRef.current = setTimeout( + () => callbackRef.current?.(Date.now()), + duration, + ); + } + + return () => { + clearTimeout(timeoutRef.current); + }; + }, [duration, disabled]); +} diff --git a/src/index.ts b/src/index.ts index c8bc191..91817bb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ export * from './hooks'; export {useVurtis, type VurtisOptions} from './useVurtis'; +export {useVurttle, VURTTLE_DURATION} from './useVurttle'; export type { VurtisListElement, diff --git a/src/useVurtis.ts b/src/useVurtis.ts index 4aacbd5..f3c4cc6 100644 --- a/src/useVurtis.ts +++ b/src/useVurtis.ts @@ -34,7 +34,6 @@ export function useVurtis({ minWidth = MIN_ITEM_SIZE, gap = 0, }: VurtisOptions) { - // const isMounted = useMounted(); const listRef = useRef(null); const [columns, setColumns] = useState(1); @@ -178,15 +177,37 @@ export function useVurtis({ }, [virtualItems]); return { + // Required props: listRef, - updateItemHeight, - listHeight, virtualItems, - rangeStart, - rangeEnd, + + // Layout strategy 1: + // Apply `listHeight` and absolutely position all items. + listHeight, + + // Layout strategy 2: // Useful for layouts that want to use a CSS grid instead of // absolute positioning. This may be necessary for animation. getSpaceBefore, getSpaceAfter, + + // Optionally pass `updateItemHeight` as `ref` (ideally + // only to 1 item) in order to more accurately measure + // item height across all resize operations. + updateItemHeight, + + // Additional props + // None of these should be required for a functional + // virtualized list, but might be useful for debugging. + columns, + rangeStart, + rangeEnd, + listWidth, + listVisibleHeight, + itemWidth, + itemHeight, + scrollY, + documentHeight, + windowHeight, }; } diff --git a/src/useVurttle.ts b/src/useVurttle.ts new file mode 100644 index 0000000..a0bed86 --- /dev/null +++ b/src/useVurttle.ts @@ -0,0 +1,36 @@ +import {useCallback, useEffect, useState} from 'react'; +import {useMounted, useTimeout} from './hooks'; + +// This hook is an opinionated "throttle" for `vurt` changes. +// The idea is that you will feed this hook a value such as +// `listWidth` or `itemHeight`. Upon receiving a new value, +// the `pending` state will become `true` and the timer will +// begin counting down before returning to `false`. This is +// useful for when you need to perform a side-effect to +// virtual container/item changes. A common use-case for +// this throttling layout animations during resize operations. +// This may be necessary to avoid very aggresive re-renders. + +export const VURTTLE_DURATION = 200; + +export function useVurttle(vurtValue = 0) { + const isMounted = useMounted(); + const [pending, setPending] = useState(false); + + const handleReset = useCallback(() => { + setPending(false); + }, []); + + useEffect(() => { + if (isMounted() && !pending && vurtValue) { + setPending(true); + } + }, [isMounted, vurtValue]); + + useTimeout(handleReset, { + duration: VURTTLE_DURATION, + disabled: !isMounted() || !pending, + }); + + return pending; +}