hookify-react
Version:
A collection of optimized and reusable React hooks for state management, dom interaction, responsive design, storage, location, asynchronous management and performance improvements.
659 lines (635 loc) • 19.7 kB
JavaScript
// src/hooks/default/useAdvancedEffect.ts
import { useEffect, useRef } from "react";
function useAdvancedEffect(effect, deps) {
const firstRender = useRef(true);
const previousDepsRef = useRef(void 0);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
previousDepsRef.current = deps;
return;
}
const hasDepsChanged = deps.some(
(dep, i) => dep !== previousDepsRef.current?.[i]
);
if (hasDepsChanged) {
previousDepsRef.current = deps;
return effect();
}
}, [deps, effect]);
}
// src/hooks/default/useUpdatedEffect.ts
import { useEffect as useEffect2, useRef as useRef2 } from "react";
function useUpdatedEffect(effect, deps) {
const previousDepsRef = useRef2(void 0);
useEffect2(() => {
previousDepsRef.current = deps;
const isDepsChanged = deps.some(
(dep, i) => dep !== previousDepsRef.current?.[i]
);
if (isDepsChanged) {
previousDepsRef.current = deps;
return effect();
}
}, [effect, deps]);
}
// src/hooks/state-management/useArray.ts
import { useRef as useRef3, useState } from "react";
function useArray(initialValue) {
const [state, setState] = useState(initialValue);
const initialValueRef = useRef3(initialValue);
const push = (value) => {
setState((prev) => [...prev, value]);
return state.length + 1;
};
const pop = () => {
const lastElement = state[state.length - 1];
setState((prev) => prev.slice(0, -1));
return lastElement;
};
const unshift = (value) => {
setState((prev) => [value, ...prev]);
return state.length + 1;
};
const shift = () => {
const firstElement = state[0];
setState((prev) => prev.slice(1));
return firstElement;
};
const removeByIndex = (index) => {
setState((prev) => [...prev.slice(0, index), ...prev.slice(index + 1)]);
};
const removeByValue = (value) => {
setState(
(prev) => prev.filter((item) => {
if (typeof value === "object") {
return JSON.stringify(item) !== JSON.stringify(value);
} else {
return item !== value;
}
})
);
};
const clear = () => {
setState([]);
};
const replace = (newArray) => {
setState(newArray);
};
const reset = () => {
setState(initialValueRef.current);
};
const filter = (predicate) => {
setState((prev) => prev.filter(predicate));
};
const updateByIndex = (index, value) => {
setState((prev) => prev.map((item, i) => i === index ? value : item));
};
const updateByValue = (prevValue, newValue) => {
setState(
(prev) => prev.map((item) => {
if (typeof item !== "object")
return item === prevValue ? newValue : item;
return JSON.stringify(item) === JSON.stringify(prevValue) ? newValue : item;
})
);
};
return [
state,
setState,
{
push,
pop,
shift,
unshift,
removeByIndex,
removeByValue,
clear,
filter,
reset,
replace,
updateByIndex,
updateByValue
}
];
}
// src/hooks/state-management/useCounter.ts
import { useRef as useRef4, useState as useState2 } from "react";
function useCounter(initialValue = 0) {
const [count, setCount] = useState2(initialValue);
const initialValueRef = useRef4(initialValue);
const increment = () => {
setCount((prev) => prev + 1);
};
const incrementByValue = (value) => {
setCount((prev) => prev + value);
};
const decrement = () => {
setCount((prev) => prev - 1);
};
const decrementByValue = (value) => {
setCount((prev) => prev - value);
};
const reset = () => {
setCount(initialValueRef.current);
};
return {
count,
increment,
incrementByValue,
decrement,
decrementByValue,
reset
};
}
// src/hooks/state-management/useFormState.ts
import {
useCallback,
useMemo,
useRef as useRef5,
useState as useState3
} from "react";
function useFormState(defaultValue, predicates, { emptyInputValidation = true } = {}) {
const resolvedDefaultValue = useMemo(
() => typeof defaultValue === "function" ? defaultValue() : defaultValue,
[defaultValue]
);
const resolvedDefaultValueRef = useRef5(resolvedDefaultValue);
const [state, setState] = useState3(resolvedDefaultValue);
const [errors, setErrors] = useState3(
Array(predicates.length).fill(void 0)
);
const validate = useCallback(
(value) => {
const newErrors = predicates.map((predicate) => predicate(value));
if (JSON.stringify(newErrors) !== JSON.stringify(errors)) {
setErrors(newErrors);
}
},
[predicates, errors]
);
const setValue = useCallback(
(value) => {
setState((prev) => {
const newValue = typeof value === "function" ? value(prev) : value;
validate(newValue);
if (typeof newValue === "string" && newValue.length === 0 && !emptyInputValidation) {
setErrors([]);
}
return newValue;
});
},
[validate, emptyInputValidation]
);
const filteredErrors = useMemo(
() => errors.filter((error) => !!error),
[errors]
);
const isValid = filteredErrors.length === 0;
const status = useMemo(() => {
if (resolvedDefaultValueRef.current === state && isValid) return "idle";
return isValid ? "valid" : "error";
}, [state, isValid]);
return [
state,
setValue,
{ errors: filteredErrors, isValid, status }
];
}
// src/hooks/state-management/useHistory.ts
import { useCallback as useCallback2, useRef as useRef6, useState as useState4 } from "react";
function useHistory(defaultValue, { capacity = 10 } = {}) {
const resolvedDefaultValue = typeof defaultValue === "function" ? defaultValue() : defaultValue;
const [state, setState] = useState4(resolvedDefaultValue);
const historyRef = useRef6([resolvedDefaultValue]);
const pointerRef = useRef6(0);
const set = useCallback2(
(value) => {
setState((prev) => {
const resolvedValue = typeof value === "function" ? value(prev) : value;
if (historyRef.current[pointerRef.current] !== resolvedValue) {
if (pointerRef.current < historyRef.current.length - 1) {
historyRef.current = historyRef.current.slice(
0,
pointerRef.current + 1
);
}
historyRef.current.push(resolvedValue);
if (historyRef.current.length > capacity) {
historyRef.current.shift();
}
pointerRef.current = historyRef.current.length - 1;
}
return resolvedValue;
});
},
[capacity]
);
const back = useCallback2(() => {
if (pointerRef.current > 0) {
pointerRef.current--;
setState(historyRef.current[pointerRef.current]);
}
}, []);
const forward = useCallback2(() => {
if (pointerRef.current < historyRef.current.length - 1) {
pointerRef.current++;
setState(historyRef.current[pointerRef.current]);
}
}, []);
const go = useCallback2((index) => {
if (index >= 0 && index < historyRef.current.length) {
pointerRef.current = index;
setState(historyRef.current[pointerRef.current]);
}
}, []);
return [
state,
set,
{
history: historyRef.current,
pointer: pointerRef.current,
back,
forward,
go
}
];
}
// src/hooks/state-management/usePrevious.ts
import { useEffect as useEffect3, useRef as useRef7 } from "react";
function usePrevious(value) {
const ref = useRef7(null);
useEffect3(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// src/hooks/state-management/useToggle.ts
import { useState as useState5 } from "react";
function useToggle(initialValue) {
const [state, setState] = useState5(initialValue);
function setValue(value) {
setState((prev) => typeof value === "boolean" ? value : !prev);
}
return [state, setValue];
}
// src/hooks/async-management/useDebounce.ts
import { useEffect as useEffect5 } from "react";
// src/hooks/async-management/useTimeout.ts
import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef8 } from "react";
function useTimeout(callback, delay) {
const callbackRef = useRef8(callback);
const timeoutRef = useRef8();
const set = useCallback3(() => {
timeoutRef.current = window.setTimeout(() => {
callbackRef.current();
}, delay);
}, [delay]);
const clear = useCallback3(() => {
if (timeoutRef.current !== void 0) {
clearTimeout(timeoutRef.current);
timeoutRef.current = void 0;
}
}, []);
const reset = useCallback3(() => {
clear();
set();
}, [clear, set]);
useEffect4(() => {
set();
return clear;
}, [set, clear]);
useEffect4(() => {
callbackRef.current = callback;
}, [callback]);
return { set, clear, reset };
}
// src/hooks/async-management/useDebounce.ts
function useDebounce(callback, delay, deps) {
const { reset, clear } = useTimeout(callback, delay);
useEffect5(reset, [...deps, reset]);
useEffect5(clear, [clear]);
}
// src/hooks/async-management/useInterval.ts
import { useEffect as useEffect6, useRef as useRef9 } from "react";
function useInterval(callback, interval = 1e3) {
const intervalRef = useRef9(null);
const clear = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
useEffect6(() => {
intervalRef.current = setInterval(callback, interval);
return clear;
}, [callback, interval]);
return { clear };
}
// src/hooks/storage/useStorage.ts
import { useEffect as useEffect7, useState as useState6 } from "react";
function useStorage(key, defaultValue, storage) {
const [value, setValue] = useState6(() => {
try {
const storedJsonValue = storage.getItem(key);
if (storedJsonValue !== null) return JSON.parse(storedJsonValue);
} catch (error) {
console.error(`Error parsing storage key "${key}":`, error);
}
return typeof defaultValue === "function" ? defaultValue() : defaultValue;
});
useEffect7(() => {
if (value === void 0) {
storage.removeItem(key);
} else {
storage.setItem(key, JSON.stringify(value));
}
}, [value, key, storage]);
return [value, setValue];
}
function useLocalStorage(key, defaultValue) {
return useStorage(key, defaultValue, window.localStorage);
}
function useSessionStorage(key, defaultValue) {
return useStorage(key, defaultValue, window.sessionStorage);
}
// src/hooks/dom/useCopyToClipboard.ts
import { useState as useState7 } from "react";
function useCopyToClipboard() {
const [isCopied, setIsCopied] = useState7(false);
const [error, setError] = useState7(null);
async function copy(text) {
try {
if (!navigator.clipboard) {
throw new Error("Clipboard API not available");
}
await navigator.clipboard.writeText(text);
setIsCopied(true);
setError(null);
setTimeout(() => setIsCopied(false), 2e3);
} catch (err) {
setError(err.message);
setIsCopied(false);
}
}
return { copy, isCopied, error };
}
// src/hooks/dom/useEventListener.ts
import { useRef as useRef10, useEffect as useEffect8 } from "react";
function useEventListener(eventType, callback, elementRef, options) {
const callbackRef = useRef10(callback);
useEffect8(() => {
callbackRef.current = callback;
}, [callback]);
useEffect8(() => {
const target = elementRef?.current ?? window;
if (!(target && target.addEventListener)) return;
const handleEvent = (event) => callbackRef.current(event);
target.addEventListener(eventType, handleEvent, options);
return () => {
target.removeEventListener(
eventType,
handleEvent,
options
);
};
}, [eventType, elementRef, options]);
}
// src/hooks/dom/useHover.ts
import { useRef as useRef11, useState as useState8, useCallback as useCallback4 } from "react";
function useHover() {
const [isHovered, setIsHovered] = useState8(false);
const ref = useRef11(null);
const handleHover = useCallback4(
(event) => setIsHovered(event.type === "mouseenter"),
[]
);
useEventListener("mouseenter", handleHover, ref);
useEventListener("mouseleave", handleHover, ref);
return { ref, isHovered };
}
// src/hooks/dom/useOnClickOutside.ts
import { useCallback as useCallback5, useRef as useRef12 } from "react";
function useClickOutside(callback) {
const ref = useRef12(null);
const handleClick = useCallback5(
(event) => {
if (!ref.current?.contains(event.target)) {
callback();
}
},
[callback, ref]
);
useEventListener("mousedown", handleClick, { current: document.body });
useEventListener("touchstart", handleClick, { current: document.body });
return { ref };
}
// src/hooks/dom/useOnlineStatus.ts
import { useState as useState9 } from "react";
function useOnlineStatus() {
const [onlineStatus, setOnlineStatus] = useState9(
navigator.onLine ? "online" : "offline"
);
const updateStatus = () => {
setOnlineStatus(navigator.onLine ? "online" : "offline");
};
useEventListener("online", updateStatus, { current: window });
return { onlineStatus };
}
// src/hooks/dom/useOnScreen.ts
import { useEffect as useEffect9, useRef as useRef13, useState as useState10 } from "react";
function useOnScreen(rootMargin = "0px") {
const [isVisible, setIsVisible] = useState10(false);
const ref = useRef13(null);
useEffect9(() => {
const target = ref.current;
if (!target) return;
const observer = new IntersectionObserver(
([entry]) => setIsVisible(entry.isIntersecting),
{ rootMargin }
);
observer.observe(target);
return () => observer.unobserve(target);
}, [ref, rootMargin]);
return { ref, isVisible };
}
// src/hooks/dom/usePress.ts
import { useRef as useRef14, useState as useState11 } from "react";
function usePress() {
const [isPressed, setIsPressed] = useState11(false);
const ref = useRef14(null);
useEventListener("mousedown", () => setIsPressed(true), ref);
useEventListener("mouseup", () => setIsPressed(false), ref);
return { isPressed, ref };
}
// src/hooks/dom/useScrollPosition.ts
import { useState as useState12, useEffect as useEffect10, useRef as useRef15, useCallback as useCallback6 } from "react";
function useScrollInfo() {
const ref = useRef15(null);
const [scrollData, setScrollData] = useState12({
scrollX: 0,
scrollY: 0,
scrollDirection: "none",
isScrolling: false,
scrollProgress: 0
});
const lastScrollY = useRef15(0);
const lastScrollX = useRef15(0);
const scrollTimeout = useRef15(null);
const handleScroll = useCallback6(() => {
const target = ref.current ?? document.documentElement;
const newScrollX = target.scrollLeft ?? window.scrollX;
const newScrollY = target.scrollTop ?? window.scrollY;
const maxScrollHeight = target.scrollHeight - target.clientHeight;
const scrollPercentage = maxScrollHeight > 0 ? newScrollY / maxScrollHeight * 100 : 0;
const directionX = newScrollX > lastScrollX.current ? "right" : newScrollX < lastScrollX.current ? "left" : "none";
const directionY = newScrollY > lastScrollY.current ? "down" : newScrollY < lastScrollY.current ? "up" : "none";
setScrollData({
scrollX: newScrollX,
scrollY: newScrollY,
scrollDirection: directionY !== "none" ? directionY : directionX,
isScrolling: true,
scrollProgress: scrollPercentage
});
lastScrollX.current = newScrollX;
lastScrollY.current = newScrollY;
if (scrollTimeout.current) clearTimeout(scrollTimeout.current);
scrollTimeout.current = setTimeout(() => {
setScrollData((prev) => ({ ...prev, isScrolling: false }));
}, 150);
}, []);
useEffect10(() => {
const target = ref.current ?? window;
target.addEventListener("scroll", handleScroll, { passive: true });
return () => target.removeEventListener("scroll", handleScroll);
}, [handleScroll]);
return { ref, ...scrollData };
}
// src/hooks/dom/useSize.ts
import { useEffect as useEffect11, useRef as useRef16, useState as useState13, useCallback as useCallback7 } from "react";
function useSize() {
const ref = useRef16(null);
const [size, setSize] = useState13(null);
const updateSize = useCallback7((entries) => {
const entry = entries[0];
if (!entry) return;
const { width, height, top, left, bottom, right } = entry.contentRect;
setSize({ width, height, top, left, bottom, right });
}, []);
useEffect11(() => {
const element = ref.current;
if (!element) return;
const observer = new ResizeObserver(updateSize);
observer.observe(element);
return () => observer.disconnect();
}, [updateSize]);
return { ref, size };
}
// src/hooks/dom/useWindowSize.ts
import { useState as useState14, useCallback as useCallback8, useLayoutEffect } from "react";
function useWindowSize() {
const isSSR = typeof window === "undefined";
const [size, setSize] = useState14({
width: isSSR ? 0 : window.innerWidth,
height: isSSR ? 0 : window.innerHeight
});
const updateSize = useCallback8(() => {
setSize({ width: window.innerWidth, height: window.innerHeight });
}, []);
useLayoutEffect(() => {
if (isSSR) return;
updateSize();
window.addEventListener("resize", updateSize);
return () => {
window.removeEventListener("resize", updateSize);
};
}, [isSSR, updateSize]);
return size;
}
// src/hooks/location/useGeoLocation.ts
import { useEffect as useEffect12, useState as useState15, useRef as useRef17 } from "react";
function useGeoLocation(options) {
const [loading, setLoading] = useState15(true);
const [error, setError] = useState15(null);
const [coords, setCoords] = useState15(null);
const retriesRef = useRef17(0);
const watchIdRef = useRef17(null);
useEffect12(() => {
if (!navigator.geolocation) {
setError({
code: 0,
message: "Geolocation is not supported by this browser."
});
setLoading(false);
return;
}
const {
enableHighAccuracy = false,
maximumAge = 0,
timeout = 1e4,
retryLimit = 3,
retryDelay = 2e3
} = options || {};
const fetchLocation = () => {
setLoading(true);
const successCallback = (position) => {
setCoords(position.coords);
setError(null);
setLoading(false);
retriesRef.current = 0;
};
const errorCallback = (positionError) => {
setError({ code: positionError.code, message: positionError.message });
if (retriesRef.current < retryLimit) {
retriesRef.current += 1;
setTimeout(fetchLocation, retryDelay);
} else {
setLoading(false);
}
};
watchIdRef.current = navigator.geolocation.watchPosition(
successCallback,
errorCallback,
{
enableHighAccuracy,
maximumAge,
timeout
}
);
};
fetchLocation();
return () => {
if (watchIdRef.current !== null) {
navigator.geolocation.clearWatch(watchIdRef.current);
}
};
}, [options]);
return { loading, error, coords };
}
export {
useAdvancedEffect,
useArray,
useClickOutside,
useCopyToClipboard,
useCounter,
useDebounce,
useEventListener,
useFormState,
useGeoLocation,
useHistory,
useHover,
useInterval,
useLocalStorage,
useOnScreen,
useOnlineStatus,
usePress,
usePrevious,
useScrollInfo,
useSessionStorage,
useSize,
useStorage,
useTimeout,
useToggle,
useUpdatedEffect,
useWindowSize
};