UNPKG

state-hooks

Version:

Essential set of React Hooks for convenient state management.

212 lines (204 loc) 7.21 kB
import { useState, useRef, useEffect, useCallback } from 'react'; /** * Tracks whether a value has changed over a relatively given period of time. * * @param value Props, state or any other calculated value. * @param {number} groupingIntervalMs Time interval, in milliseconds, to group a batch of changes by. * @returns `true` if the value has changed at least once over the given interval, or `false` otherwise. * * @example * function Component() { * const scrollCoords = useWindowScrollCoords(); * const isScrolling = useChanging(scrollCoords); * // ... * } */ function useChanging(value, groupingIntervalMs = 150) { const [isChanging, setChanging] = useState(false); const prevGroupingIntervalMsRef = useRef(0); useEffect(() => { // Prevent initial state from being true if (groupingIntervalMs !== prevGroupingIntervalMsRef.current) { prevGroupingIntervalMsRef.current = groupingIntervalMs; } else { setChanging(true); } const timeoutID = setTimeout(() => setChanging(false), groupingIntervalMs); return () => { clearTimeout(timeoutID); }; }, [groupingIntervalMs, value]); return isChanging; } /** * Tracks previous state of a value. * * @param value Props, state or any other calculated value. * @returns Value from the previous render of the enclosing component. * * @example * function Component() { * const [count, setCount] = useState(0); * const prevCount = usePrevious(count); * // ... * return `Now: ${count}, before: ${prevCount}`; * } */ function usePrevious(value) { // Source: https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; } // Source: https://v8.dev/blog/react-cliff#value-representation // eslint-disable-next-line import/prefer-default-export const MAX_SMALL_INTEGER = 2 ** 30 - 1; /** * Records states of a value over time. * * @param value Props, state or any other calculated value. * @param maxLength Maximum amount of states to store at once. Should be an integer greater than 1. * @returns Results of state updates in chronological order. * * @example * function Component() { * const [count, setCount] = useState(0); * const counts = useTimeline(count); * // ... * return `Now: ${count}, history: ${counts}`; * } */ function useTimeline(value, maxLength = MAX_SMALL_INTEGER) { const valuesRef = useRef([]); const prevValue = usePrevious(value); if (!Object.is(value, prevValue)) { // Use immutable refs to behave like state variables valuesRef.current = [...valuesRef.current, value]; } if (valuesRef.current.length > maxLength) { valuesRef.current.splice(0, valuesRef.current.length - maxLength); } return valuesRef.current; } /* eslint-disable jsdoc/valid-types */ /** * Wraps a state hook to add boolean toggle functionality. * * @param useStateResult Return value of a state hook. * @param useStateResult.0 Current state. * @param useStateResult.1 State updater function. * @returns State hook result extended with a `toggle` function. * * @example * function Component() { * const [isPressed, setPressed, togglePressed] = useToggle( * useState<boolean>(false), * ); * // ... * return ( * <button type="button" aria-pressed={isPressed} onClick={togglePressed}> * Toggle state * </button> * ); * } */ function useToggle([value, setValue]) { const toggleValue = useCallback(() => { setValue((prevValue) => !prevValue); }, [setValue]); return [value, setValue, toggleValue]; } /** * Wraps a state hook to add undo/redo functionality. * * @param useStateResult Return value of a state hook. * @param useStateResult.0 Current state. * @param useStateResult.1 State updater function. * @param maxDeltas Maximum amount of state differences to store at once. Should be a positive integer. * @returns State hook result extended with an object containing `undo`, `redo`, `past`, `future` and `jump`. * * @example * function Component() { * const [value, setValue, { undo, redo, past, future }] = useUndoable( * useState(''), * ); * // ... * return ( * <> * <button type="button" onClick={undo} disabled={past.length === 0}> * Undo * </button> * <input value={value} onChange={(event) => setValue(event.target.value)} /> * <button type="button" onClick={redo} disabled={future.length === 0}> * Redo * </button> * </> * ); * } */ function useUndoable([value, setValue], maxDeltas = MAX_SMALL_INTEGER) { // Source: https://redux.js.org/recipes/implementing-undo-history const pastValuesRef = useRef([]); const futureValuesRef = useRef([]); const newSetValue = useCallback((update) => { setValue((prevValue) => { futureValuesRef.current = []; pastValuesRef.current = [...pastValuesRef.current, prevValue]; return typeof update === 'function' ? update(prevValue) : update; }); }, [setValue]); const jump = useCallback((delta) => { if (delta < 0 && pastValuesRef.current.length >= -delta) { // Undo setValue((prevValue) => { const nextValueIndex = pastValuesRef.current.length + delta; const nextValue = pastValuesRef.current[nextValueIndex]; futureValuesRef.current = [ ...pastValuesRef.current.slice(nextValueIndex + 1), prevValue, ...futureValuesRef.current, ]; pastValuesRef.current = pastValuesRef.current.slice(0, delta); return nextValue; }); } else if (delta > 0 && futureValuesRef.current.length >= delta) { // Redo setValue((prevValue) => { const nextValue = futureValuesRef.current[delta - 1]; pastValuesRef.current = [ ...pastValuesRef.current, prevValue, ...futureValuesRef.current.slice(0, delta - 1), ]; futureValuesRef.current = futureValuesRef.current.slice(delta); return nextValue; }); } }, [setValue]); const undo = useCallback(() => jump(-1), [jump]); const redo = useCallback(() => jump(+1), [jump]); const deltas = pastValuesRef.current.length + futureValuesRef.current.length; if (deltas > maxDeltas) { futureValuesRef.current.splice(maxDeltas - deltas, MAX_SMALL_INTEGER); pastValuesRef.current.splice(0, pastValuesRef.current.length - maxDeltas); } return [ value, newSetValue, { undo, redo, past: pastValuesRef.current, future: futureValuesRef.current, jump, }, ]; } export { useChanging, usePrevious, useTimeline, useToggle, useUndoable }; //# sourceMappingURL=index.js.map