UNPKG

@supunlakmal/hooks

Version:

A collection of reusable React hooks

138 lines 7 kB
import { useCallback, useState, useMemo, useRef } from 'react'; // --- useSyncedRef (Seems OK, no changes needed) --- /** * Like `useRef`, but it returns immutable ref that contains actual value. * Ensures the .current getter always provides the latest value passed to the hook. * @param value The value to track. */ const useSyncedRef = (value) => { const ref = useRef(value); // Update the ref's current value on every render ref.current = value; // Return a memoized, frozen object with a getter // The object identity is stable, but the getter always reads the latest ref.current return useMemo(() => Object.freeze({ get current() { return ref.current; }, }), [] // Empty dependency array is correct here ); }; /** Internal helper to resolve initial state */ const resolveInitialState = (initialState) => { // Check if initialState is a function (initializer) and call it if (typeof initialState === 'function') { // We cast here because TS struggles with the typeof check narrowing perfectly in this context return initialState(); } return initialState; }; /** Internal helper to resolve next state based on previous state */ const resolveNextState = (nextState, previousState) => { // Check if nextState is a function (updater) and call it with previousState if (typeof nextState === 'function') { // We cast here for the same reason as above return nextState(previousState); } return nextState; }; // --- useMediatedState (Corrected) --- /** * Like `useState`, but every value set is passed through a mediator function * before the state is updated. The initial state is NOT mediated by default. * @template State The type of the state managed by the hook (after mediation). * @template RawState The type of the value passed to the setter (before mediation). Defaults to State. * @param initialState Initial state value or initializer function. Resolved value is used directly. * @param mediator Optional function that takes the resolved raw value and returns the final state value. Applied only on updates. */ export const useMediatedState = (initialState, mediator) => { // Use useSyncedRef to track the latest mediator function without causing setter recreation const mediatorRef = useSyncedRef(mediator); // Initialize state using the provided initialState helper // The initial state itself is NOT mediated here. Mediation happens on updates. const [state, setState] = useState(() => resolveInitialState(initialState)); // Memoize the setter function const setMediatedState = useCallback((valueOrFn) => { // Use the functional update form of setState to ensure we have the latest previous state setState((prevState) => { // 1. Resolve the raw value (handles both direct value and updater function) const resolvedRawValue = resolveNextState(valueOrFn, prevState); // 2. Get the current mediator function from the ref const currentMediator = mediatorRef.current; // 3. Apply mediator if it exists, otherwise return the resolved raw value if (currentMediator) { return currentMediator(resolvedRawValue); } else { // If no mediator, assume RawState is assignable to State // Add type assertion for safety, though ideally State == RawState here // Or the user should ensure compatibility or provide an identity mediator. return resolvedRawValue; } }); }, [] // setState is stable, mediatorRef handles mediator changes. No deps needed. ); // Return the current state and the memoized setter return [state, setMediatedState]; }; /** * Tracks a numeric value with optional min/max boundaries and provides counter actions. * * @param initialValue The initial value (or initializer function). Defaults to 0. * @param max Optional maximum value. Initial value is clamped if needed. * @param min Optional minimum value. Initial value is clamped if needed. */ export const useCounter = (initialValue = 0, max, min) => { // 1. Define the clamping function (mediator logic) based on min/max const clamp = useCallback((value) => { let clampedValue = value; if (max !== undefined) { clampedValue = Math.min(max, clampedValue); } if (min !== undefined) { clampedValue = Math.max(min, clampedValue); } return clampedValue; }, [max, min]); // Recreate clamp only if min/max change // 2. Resolve and clamp the *initial* value *once* for the reset functionality // and to ensure the starting state is valid. const initialClampedValue = useMemo(() => { const resolvedInitial = resolveInitialState(initialValue); return clamp(resolvedInitial); // eslint-disable-next-line react-hooks/exhaustive-deps }, [clamp]); // Depends on the clamp function (which depends on min/max) // initialValue is only needed on mount, but including clamp ensures // if min/max change the initial value *conceptually* aligns, though reset stays fixed. // Alternatively, use [] if you strictly want the *first ever* clamped value. // Using [clamp] makes slightly more sense if min/max could change, though unusual for an initial value. // 3. Use useMediatedState with the initially clamped value and the clamp function as mediator const [state, setState] = useMediatedState(initialClampedValue, // Start with the correctly clamped initial value clamp // Use clamp as the mediator for subsequent updates ); // 4. Define stable actions using the setter from useMediatedState const actions = useMemo(() => ({ // `get` is removed - the state value is returned directly by the hook set: (value) => { // Pass value/fn directly; useMediatedState handles resolution and mediation (clamping) setState(value); }, inc: (delta = 1) => { // setState uses functional update. We resolve delta *inside* it. // The outer value passed to setState is the updater function. setState((prev) => prev + resolveNextState(delta, prev)); }, dec: (delta = 1) => { setState((prev) => prev - resolveNextState(delta, prev)); }, reset: () => { // Reset directly to the stored initial clamped value. // This ensures reset goes back to the *first* clamped value, // even if min/max props change later. setState(initialClampedValue); }, // Note: state is not needed in deps here because actions only depend on setState and initialClampedValue // which are stable or memoized correctly. }), [setState, initialClampedValue]); return [state, actions]; }; //# sourceMappingURL=useCounter.js.map