UNPKG

state-hooks

Version:

Essential set of React Hooks for convenient state management.

216 lines (194 loc) 6.86 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var react = require('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] = react.useState(false); const prevGroupingIntervalMsRef = react.useRef(0); react.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 = react.useRef(); react.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 = react.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 = react.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 = react.useRef([]); const futureValuesRef = react.useRef([]); const newSetValue = react.useCallback(update => { setValue(prevValue => { futureValuesRef.current = []; pastValuesRef.current = [...pastValuesRef.current, prevValue]; return typeof update === 'function' ? update(prevValue) : update; }); }, [setValue]); const jump = react.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 = react.useCallback(() => jump(-1), [jump]); const redo = react.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 }]; } exports.useChanging = useChanging; exports.usePrevious = usePrevious; exports.useTimeline = useTimeline; exports.useToggle = useToggle; exports.useUndoable = useUndoable; //# sourceMappingURL=index.js.map