UNPKG

@exabytellc/utils

Version:

EB react utils to make everything a little easier!

589 lines (554 loc) 20.3 kB
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { Cookie, LocaleStorage, SessionStorage } from "../storage"; import { isFunc, isObj } from "../types"; /** * Creates a custom hook with an optional initializer function. * * This function generates a hook (`useHook`) that can accept and return a context value, * which can be customized by providing additional props through `withProps`. * * @param {Function} [initializer=() => {}] - A function to initialize the state with given parameters. * @returns {Object} - The custom hook object with `useHook` and `withProps` for additional customization. */ export function createStateHook(initializer = () => { }) { /** * Custom hook to access and manipulate the state context. * * @param {*} state - The current state to be used by the initializer function. * @param {Object} [props] - Optional props to customize the state. * @returns {*} - The initialized context value based on the provided state and props. * @throws {Error} - If the hook is used outside of a context provider. */ const useHook = ([value, setValue, _obj = {}], props) => { return [value, setValue, { ..._obj, ...(initializer([value, setValue], props) ?? {}) }]; }; /** * Higher-order function to enhance the context with additional properties. * * @param {Object} props - Additional properties for the context, extending the `useHook` functionality. * @returns {Function} - Returns the modified hook with additional context props. */ const withProps = (props) => { useHook.props = props; return useHook; }; // Attach the `withProps` function to the `useHook` to allow additional customization. useHook.withProps = withProps; return useHook; } /** * A custom hook to apply multiple hooks with optional props. * * @param {any} initialState - The initial state value. * @param {Array} hooks - An array of hooks to apply, where each hook is a function. * @returns {Array} - The current state, a function to set the state, and an object containing each hook's methods. */ export function StateHooks(initialValue, hooks = []) { const [value, setValue] = useState(initialValue); return hooks.reduce((prev, hook) => hook(prev, hook?.props), [value, setValue]); } /** * A custom hook that provides toggle and reset functionality. * * @param {[boolean, Function]} param0 - The current value and setter function. * @param {Object} [_obj={}] - Optional additional object to spread into the return. * @returns {[boolean, Function, Object]} The current value, setter function, and toggle/reset methods. */ export const ToggleState = createStateHook(([value, setValue]) => { const initial = useRef(value); const toggle = useCallback(() => { setValue((prev) => !prev); }, [setValue]); const reset = useCallback(() => { setValue(initial.current); }, [setValue]); return { toggle, reset }; }); /** * A custom hook that extends ToggleState to manage modal open/close state. * * @param {[boolean, Function]} param0 - The current value and setter function. * @param {Object} [_obj={}] - Optional additional object to spread into the return. * @returns {[boolean, Function, Object]} The current value, setter function, and modal methods. */ export const ModalState = createStateHook(([value, setValue]) => { const [, , { toggle, reset }] = ToggleState([value, setValue]); const open = useCallback(() => { setValue(true); }, [setValue]); const close = useCallback(() => { setValue(false); }, [setValue]); return { toggle, reset, open, close }; }); /** * A custom hook managing state stored in a cookie. * * @param {[any, Function]} param0 - The current value and setter function. * @param {Object} options - Cookie configuration options. * @param {string} options.name - The name of the cookie. * @param {number} [options.expireHours=null] - Expiration time in hours. * @param {boolean} [options.json=false] - Flag indicating if the value is JSON. * @returns {[any, Function, Object]} An array where: * - The first element is the cookie-stored state. * - The second element is the setter function. * - The third element is an object with save, refresh, and additional cookie methods. */ export const CookieState = createStateHook(([value, setValue], { name, expireHours = null, json = false } = {}) => { const initial = useRef(value); const save = useCallback( (value) => { if (value) { Cookie.set(name, value, { expireHours, json }); } else { Cookie.del(name); } }, [name, expireHours, json] ); const refresh = useCallback(() => { setValue(value); }, [setValue, value]); // initial value useLayoutEffect(() => { setValue(() => Cookie.get(name, { json }) ?? initial.current); }, [json, name]); // save onchange useLayoutEffect(() => { save(value); }, [value, save]); // check cookie expired? useEffect(() => { if (value && value != initial.current) { const interval = setInterval(() => { if (!Cookie.get(name)) setValue(initial.current) }, expireHours * 3600000 + 1000); return () => clearInterval(interval); } }, [expireHours, name, setValue, value]); return { save, refresh }; }); /** * A custom hook that manages local storage state. * * @param {[any, Function]} param0 - The current value and setter function. * @param {Object} options - Configuration options for local storage. * @param {string} options.name - The name of the local storage key. * @param {boolean} [options.json=false] - Flag to indicate JSON parsing. * @returns {[any, Function, Object]} The current value, setter function, and save method. */ export const LocalStorageState = createStateHook(([value, setValue], { name, json = false } = {}) => { const initial = useRef(value); useLayoutEffect(() => { setValue(() => LocaleStorage.get(name, { json }) ?? initial.current); }, [json, name]); const save = useCallback( (value) => { if (value) { LocaleStorage.set(name, value, { json }); } else { LocaleStorage.del(name); } }, [name, json] ); useLayoutEffect(() => { save(value); }, [value, save]); return { save }; }); /** * A custom hook that manages session storage state. * * @param {[any, Function]} param0 - The current value and setter function. * @param {Object} options - Configuration options for session storage. * @param {string} options.name - The name of the session storage key. * @param {boolean} [options.json=false] - Flag to indicate JSON parsing. * @returns {[any, Function, Object]} The current value, setter function, and save method. */ export const SessionStorageState = createStateHook(([value, setValue], { name, json = false } = {}) => { const initial = useRef(value); useLayoutEffect(() => { setValue(() => SessionStorage.get(name, { json }) ?? initial.current); }, [json, name]); const save = useCallback( (value) => { if (value) { SessionStorage.set(name, value, { json }); } else { SessionStorage.del(name); } }, [name, json] ); useLayoutEffect(() => { save(value); }, [value, save]); return { save }; }); /** * A custom hook that manages a list with various utility methods. * * @param {[Array, Function]} param0 - The current value and setter function. * @param {Object} [_obj={}] - Optional additional object to spread into the return. * @returns {[Array, Function, Object]} The current value, setter function, and list utility methods. */ export const ListState = createStateHook(([value, setValue]) => { const indexWhere = useCallback( (where) => value.findIndex(where), [value] ); const findWhere = useCallback( (where) => value.find(where), [value] ); const add = useCallback( (item) => { setValue((p) => [...p, (isFunc(item) ? item(p) : item)]); }, [setValue] ); const addAfter = useCallback( (item, index) => { setValue((prev) => [ ...prev.slice(0, index + 1), isFunc(item) ? item(prev) : item, ...prev.slice(index + 1), ]); }, [setValue] ); const update = useCallback( (item, i) => { setValue((p) => { const v = [...p]; if (v?.[i]) v[i] = (isFunc(item) ? item(p) : item); return v; }); }, [setValue] ); const updateWhere = useCallback( (item, where) => { const i = indexWhere(where); if (i > -1) update(item, i); }, [indexWhere, update] ); const remove = useCallback( (i) => { setValue((p) => { const v = [...p]; v.splice(i, 1); return v; }); }, [setValue] ); const removeWhere = useCallback( (where) => { const i = indexWhere(where); if (i > -1) remove(i); }, [indexWhere, remove] ); return { indexWhere, findWhere, add, addAfter, update, updateWhere, remove, removeWhere }; }); /** * A custom hook that manages a map with various utility methods. * * @param {[Object, Function]} param0 - The current value and setter function. * @param {Object} [_obj={}] - Optional additional object to spread into the return. * @returns {[Object, Function, Object]} The current value, setter function, and map utility methods. */ export const MapState = createStateHook(([value, setValue]) => { const keys = useMemo(() => Object.keys(value), [value]); const values = useMemo(() => Object.values(value), [value]); const keyWhere = useCallback( (where) => keys?.[values.findIndex(where)], [keys, values] ); const findWhere = useCallback( (where) => values.find(where), [values] ); const set = useCallback( (key, item) => { setValue((p) => ({ ...p, [key]: (isFunc(item) ? item(p) : item) })); }, [setValue] ); const update = useCallback( (item, key) => { setValue((p) => { const v = { ...p }; if (v?.[key]) v[key] = (isFunc(item) ? item(p) : item); return v; }); }, [setValue] ); const updateWhere = useCallback( (item, where) => { const key = keyWhere(where); if (key) update(item, key); }, [keyWhere, update] ); const remove = useCallback( (key) => { setValue((p) => { const v = { ...p }; if (v?.[key]) delete v[key]; return v; }); }, [setValue] ); const removeWhere = useCallback( (where) => { const key = keyWhere(where); if (key) remove(key); }, [keyWhere, remove] ); return { keys, values, keyWhere, findWhere, set, update, updateWhere, remove, removeWhere }; }); /** * A custom hook that manages a list of maps with various utility methods. * * @param {[Object, Function]} param0 - The current value and setter function. * @param {Object} [_obj={}] - Optional additional object to spread into the return. * @returns {[Object, Function, Object]} The current value, setter function, and map utility methods. */ export const DataState = createStateHook(([value, setValue]) => { const keys = useMemo(() => Object.keys(value), [value]); const values = useMemo(() => Object.values(value), [value]); const keyWhere = useCallback( (where) => keys?.[values.findIndex(where)], [keys, values] ); const findWhere = useCallback( (where) => values.find(where), [values] ); const set = useCallback( (key, item) => { setValue((p) => ({ ...p, [key]: (isFunc(item) ? item(p) : item) })); }, [setValue] ); const update = useCallback( (item, key) => { setValue((p) => { const v = { ...p }; if (v?.[key]) v[key] = (isFunc(item) ? item(p) : item); return v; }); }, [setValue] ); const updateWhere = useCallback( (item, where) => { const key = keyWhere(where); if (key) update(item, key); }, [keyWhere, update] ); const remove = useCallback( (key) => { setValue((p) => { const v = { ...p }; if (v?.[key]) delete v[key]; return v; }); }, [setValue] ); const removeWhere = useCallback( (where) => { const key = keyWhere(where); if (key) remove(key); }, [keyWhere, remove] ); return { keys, values, keyWhere, findWhere, set, update, updateWhere, remove, removeWhere }; }); /** * A custom hook that provides a debounced setter function for a state value. * * @param {[any, Function, Object]} param0 - An array containing the current value, setter function, and optional object. * @returns {[any, Function, Object]} - The current value, setter function, and an object containing the debounced setter. */ export const DebounceState = createStateHook(([, setValue]) => { // timeout const timeout = useRef(null); /** * Sets the value with a debounce delay. * * @param {any} val - The value to set after the delay. * @param {number} [delay=1000] - The debounce delay in milliseconds. */ const setDebounced = useCallback( (val, delay = 1000) => { clearTimeout(timeout.current); timeout.current = setTimeout(() => setValue((prev) => (isFunc(val) ? val(prev) : val)), delay); }, [setValue] ); return { setDebounced }; }); /** * A custom hook that monitors a media query and updates the state based on its match status. * * @param {[boolean, Function, Object]} param0 - An array containing the current match status, setter function, and optional object. * @param {Object} options - An object containing the media query string. * @param {string} options.query - The media query to listen for changes. * @returns {[boolean, Function, Object]} - The current match status, setter function, and optional object. */ export const MediaQueryState = createStateHook(([, setValue], { query }) => { useEffect(() => { const media = window.matchMedia(query); setValue(media.matches); // Set initial value based on the media query match status const listener = () => setValue(media.matches); // Update state when media query status changes media.addEventListener("change", listener); // Listen for changes in media query match status // Cleanup function to remove the listener return () => { media.removeEventListener("change", listener); }; }, [query]); }); /** * A custom hook that monitors multiple media queries and updates the state based on their match status. * * @param {[Object, Function]} param0 - A tuple containing the current match status object and the state setter function. * @param {Object} options - Options containing the media queries to monitor. * @param {Object} options.queries - An object where keys are query names and values are media query strings. * @returns {Object} - Returns an object with: * - `ifQuery`: A function to check if a specific media query matches. * - `ifQueries`: A function to check if multiple media queries match based on a logical condition. * - `breakpointValue`: A function to return a value based on the current active query with fallback to the previous one. * * @example * // Usage example * const [matches, setMatches, { ifQuery, ifQueries, breakpointValue }] = MediaQueriesState(useState({}),{ * queries: { * mobile: "(max-width: 600px)", * tablet: "(min-width: 601px) and (max-width: 960px)", * desktop: "(min-width: 961px)" * } * }); * * // Check if a specific query matches * const isMobile = ifQuery("mobile"); // true or false * * // Check if multiple queries match * const isMobileOrTablet = ifQueries(["mobile", "tablet"]); // true if either matches * const isDesktopOnly = ifQueries(["desktop"], "AND"); // true if desktop matches * * const width = breakpointValue(100, null, 800); // will return value of match or fallback to non null smaller value * const height = breakpointValue({mobile: 100, desktop: 300}); // will return value of match or fallback to non null smaller value * */ export const MediaQueriesState = createStateHook(([value, setValue], { queries }) => { useEffect(() => { const media = {}; /** * Updates the state with the current match status for all media queries. */ const updateMatches = () => { const newMatches = {}; let changed = false; for (const key in media) { const match = media[key].matches; newMatches[key] = match; if (value?.[key] !== match) { changed = true; } } if (changed) { setValue(newMatches); } }; // Initialize media query listeners for (const key in queries) { const query = queries[key]; media[key] = window.matchMedia(query); media[key].addEventListener("change", updateMatches); } // Set initial match status updateMatches(); // Cleanup function to remove listeners return () => { for (const key in media) { media[key].removeEventListener("change", updateMatches); } }; }, [queries, setValue, value]); /** * Checks if a specific media query currently matches. * * @param {string} query - The name of the query to check (must be a key from the `queries` object). * @returns {boolean} - Returns `true` if the media query matches, otherwise `false`. */ const ifQuery = useCallback((query) => value?.[query]?.matches, [value]); /** * Checks if multiple media queries match based on a logical condition. * * @param {string[]} queries - An array of query names to check. * @param {"AND"|"OR"} [logic="OR"] - The logical operator to apply: * - `"OR"` (default): Returns `true` if at least one query matches. * - `"AND"`: Returns `true` only if all queries match. * @returns {boolean} - The result of applying the logical operator to the match results. */ const ifQueries = useCallback( (queries, logic = "OR") => { if (!Array.isArray(queries)) { console.error("ifQueries expects an array of query names."); return false; } if (!value) return false; if (logic === "AND") { return queries.every((query) => value?.[query]?.matches); } // Default is "OR" return queries.some((query) => value?.[query]?.matches); }, [value] ); /** * Resolves the most appropriate value based on the largest matching media query. * * Iterates from the largest to smallest breakpoint (based on the order in `queries`) * and returns the first non-null corresponding value passed in `args`. * * @param {...*} args - A prioritized list of values corresponding to each media query in order, * from smallest to largest (e.g., [xsValue, smValue, mdValue, ...]). * Use `null` to skip a value for a specific breakpoint. * @returns {*} - The value corresponding to the largest matching media query, or the next best fallback. */ const breakpointValue = useCallback( (...args) => { if (isObj(args?.[0])) { const obj = args?.[0]; const queryKeys = Object.keys(queries); args = queryKeys.map((key) => obj?.[key] ?? null); } if (!value) return args.find((v) => v != null); const queryKeys = Object.keys(queries); for (let i = queryKeys.length - 1; i >= 0; i--) { const key = queryKeys[i]; if (value[key]) { if (args[i] != null) return args[i]; // Fallback to earlier non-null value for (let j = i - 1; j >= 0; j--) { if (args[j] != null) return args[j]; } } } return null }, [value, queries] ); return { ifQuery, ifQueries, breakpointValue }; });