From 5ba6dd3fa1fcf44b9b03aecc166fb0a4b9241ce8 Mon Sep 17 00:00:00 2001 From: debabin Date: Tue, 30 Jul 2024 16:24:32 +0700 Subject: [PATCH] =?UTF-8?q?main=20=F0=9F=A7=8A=20fix=20internal=20ref?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../useClickOutside/useClickOutside.demo.tsx | 4 +- src/hooks/useClickOutside/useClickOutside.ts | 20 ++--- src/hooks/useCssVar/useCssVar.demo.tsx | 13 +++ src/hooks/useCssVar/useCssVar.ts | 34 +++++++ .../useEventListener.demo.tsx | 4 - .../useEventListener/useEventListener.ts | 25 ++---- .../useInfiniteScroll/useInfiniteScroll.ts | 16 ++-- .../useIntersectionObserver.ts | 55 ++---------- src/hooks/useLongPress/useLongPress.demo.tsx | 4 +- src/hooks/useLongPress/useLongPress.ts | 89 ++++++++----------- src/hooks/useMouse/useMouse.ts | 20 ++--- .../useResizeObserver/useResizeObserver.ts | 18 ++-- src/utils/helpers/getElement.ts | 28 ++++++ src/utils/helpers/index.ts | 1 + 14 files changed, 154 insertions(+), 177 deletions(-) create mode 100644 src/hooks/useCssVar/useCssVar.demo.tsx create mode 100644 src/hooks/useCssVar/useCssVar.ts create mode 100644 src/utils/helpers/getElement.ts diff --git a/src/hooks/useClickOutside/useClickOutside.demo.tsx b/src/hooks/useClickOutside/useClickOutside.demo.tsx index dbda8fb0..cf5ac2a0 100644 --- a/src/hooks/useClickOutside/useClickOutside.demo.tsx +++ b/src/hooks/useClickOutside/useClickOutside.demo.tsx @@ -5,14 +5,14 @@ import { useClickOutside } from './useClickOutside'; const Demo = () => { const counter = useCounter(); - const ref = useClickOutside(() => { + const clickOutsideRef = useClickOutside(() => { console.log('click outside'); counter.inc(); }); return (
| (() => Element) | Element; @@ -53,19 +51,18 @@ export type UseClickOutside = { * const ref = useClickOutside(() => console.log('click outside')); */ export const useClickOutside = ((...params: any[]) => { - const rerender = useRerender(); const target = (typeof params[1] === 'undefined' ? undefined : params[0]) as | UseClickOutsideTarget | UseClickOutsideTarget[] | undefined; const callback = (params[1] ? params[1] : params[0]) as (event: Event) => void; - const internalRef = useRef(); + const [internalRef, setInternalRef] = useState(); const internalCallbackRef = useRef(callback); internalCallbackRef.current = callback; useEffect(() => { - if (!target && !internalRef.current) return; + if (!target && !internalRef) return; const handler = (event: Event) => { if (Array.isArray(target)) { if (!target.length) return; @@ -80,7 +77,7 @@ export const useClickOutside = ((...params: any[]) => { return; } - const element = target ? getElement(target) : internalRef.current; + const element = target ? getElement(target) : internalRef; if (element && !element.contains(event.target as Node)) { internalCallbackRef.current(event); @@ -94,13 +91,8 @@ export const useClickOutside = ((...params: any[]) => { document.removeEventListener('mousedown', handler); document.removeEventListener('touchstart', handler); }; - }, [internalRef.current, target]); + }, [internalRef, target]); if (target) return; - return (node: Element) => { - if (!internalRef.current) { - internalRef.current = node; - rerender.update(); - } - }; + return setInternalRef; }) as UseClickOutside; diff --git a/src/hooks/useCssVar/useCssVar.demo.tsx b/src/hooks/useCssVar/useCssVar.demo.tsx new file mode 100644 index 00000000..73c607a4 --- /dev/null +++ b/src/hooks/useCssVar/useCssVar.demo.tsx @@ -0,0 +1,13 @@ +import { useCssVar } from './useCssVar'; + +const Demo = () => { + const counter = useCssVar('--count'); + + return ( +

+ Count: {counter} +

+ ); +}; + +export default Demo; diff --git a/src/hooks/useCssVar/useCssVar.ts b/src/hooks/useCssVar/useCssVar.ts new file mode 100644 index 00000000..ab7f03c1 --- /dev/null +++ b/src/hooks/useCssVar/useCssVar.ts @@ -0,0 +1,34 @@ +import { useState } from 'react'; + +import { useMutationObserver } from '../useMutationObserver'; + +/** + * @name useCssVar + * @description - Hook that returns the value of a CSS variable + * @category Utilities + * + * @param {string} key The CSS variable key + * @param {string} initialValue The initial value of the CSS variable + * @returns {string} The value of the CSS variable + * + * @example + * const value = useCssVar('color', 'red'); + */ +export const useCssVar = (key: string, initialValue: string) => { + const [value, setValue] = useState(initialValue); + + const updateCssVar = () => { + const value = window + .getComputedStyle(window?.document?.documentElement) + .getPropertyValue(key) + ?.trim(); + + setValue(value ?? initialValue); + }; + + useMutationObserver(updateCssVar, { + attributeFilter: ['style', 'class'] + }); + + return value; +}; diff --git a/src/hooks/useEventListener/useEventListener.demo.tsx b/src/hooks/useEventListener/useEventListener.demo.tsx index 9998b55e..165b6996 100644 --- a/src/hooks/useEventListener/useEventListener.demo.tsx +++ b/src/hooks/useEventListener/useEventListener.demo.tsx @@ -9,10 +9,6 @@ const Demo = () => { } ); - useEventListener(window, 'click', (event) => console.log('@click 2', event.target), { - passive: true - }); - return (
{ - if (typeof target === 'function') { - return target(); - } - - if (target instanceof Element || target instanceof Window || target instanceof Document) { - return target; - } - - return target.current; -}; - export type UseEventListenerOptions = boolean | AddEventListenerOptions; export type UseEventListenerReturn = RefObject; @@ -78,20 +68,21 @@ export const useEventListener = ((...params: any[]) => { const listener = (target ? params[2] : params[1]) as (...arg: any[]) => void | undefined; const options: UseEventListenerOptions | undefined = target ? params[3] : params[2]; - const internalRef = useRef(null); + const [internalRef, setInternalRef] = useState(); const internalListener = useEvent(listener); useEffect(() => { + if (!target && !internalRef) return; const callback = (event: Event) => internalListener(event); - const element = target ? getElement(target) : internalRef.current; + const element = target ? getElement(target) : internalRef; if (element) { events.forEach((event) => element.addEventListener(event, callback, options)); return () => { events.forEach((event) => element.removeEventListener(event, callback, options)); }; } - }, [target, event, options]); + }, [target, internalRef, event, options]); if (target) return; - return internalRef; + return setInternalRef; }) as UseEventListener; diff --git a/src/hooks/useInfiniteScroll/useInfiniteScroll.ts b/src/hooks/useInfiniteScroll/useInfiniteScroll.ts index ffa6e1ed..e2ad280d 100644 --- a/src/hooks/useInfiniteScroll/useInfiniteScroll.ts +++ b/src/hooks/useInfiniteScroll/useInfiniteScroll.ts @@ -2,7 +2,6 @@ import type { RefObject } from 'react'; import { useEffect, useRef, useState } from 'react'; import { useEvent } from '../useEvent/useEvent'; -import { useRerender } from '../useRerender/useRerender'; /** The use infinite scroll target element type */ export type UseInfiniteScrollTarget = RefObject | (() => Element) | Element; @@ -71,7 +70,6 @@ export type UseInfiniteScroll = { * const isLoading = useInfiniteScroll(ref, () => console.log('infinite scroll')); */ export const useInfiniteScroll = ((...params) => { - const rerender = useRerender(); const target = params[1] instanceof Function ? (params[0] as UseInfiniteScrollTarget) : undefined; const callback = params[1] instanceof Function ? params[1] : (params[0] as () => void); const options = ( @@ -82,7 +80,7 @@ export const useInfiniteScroll = ((...params) => { const distance = options?.distance ?? 10; const [isLoading, setIsLoading] = useState(false); - const internalRef = useRef(); + const [internalRef, setInternalRef] = useState(); const internalCallbackRef = useRef(callback); internalCallbackRef.current = callback; @@ -108,7 +106,8 @@ export const useInfiniteScroll = ((...params) => { }); useEffect(() => { - const element = target ? getElement(target) : internalRef.current; + if (!target && !internalRef) return; + const element = target ? getElement(target) : internalRef; if (!element) return; element.addEventListener('scroll', onLoadMore); @@ -116,16 +115,11 @@ export const useInfiniteScroll = ((...params) => { return () => { element.removeEventListener('scroll', onLoadMore); }; - }, [internalRef.current, target, direction, distance]); + }, [internalRef, target, direction, distance]); if (target) return isLoading; return { - ref: (node: Element) => { - if (!internalRef.current) { - internalRef.current = node; - rerender.update(); - } - }, + ref: setInternalRef, isLoading }; }) as UseInfiniteScroll; diff --git a/src/hooks/useIntersectionObserver/useIntersectionObserver.ts b/src/hooks/useIntersectionObserver/useIntersectionObserver.ts index 2ea81d36..2305a0ae 100644 --- a/src/hooks/useIntersectionObserver/useIntersectionObserver.ts +++ b/src/hooks/useIntersectionObserver/useIntersectionObserver.ts @@ -1,7 +1,7 @@ import type { RefObject } from 'react'; import { useEffect, useRef, useState } from 'react'; -import { useRerender } from '../useRerender/useRerender'; +import { getElement } from '@/utils/helpers'; /** The intersection observer target element type */ export type UseIntersectionObserverTarget = @@ -16,32 +16,6 @@ export interface UseIntersectionObserverOptions extends Omit; } -const getTargetElement = (target: UseIntersectionObserverTarget) => { - if (typeof target === 'function') { - return target(); - } - - if (target instanceof Element) { - return target; - } - - return target.current; -}; - -const getRootElement = (root: UseIntersectionObserverOptions['root']) => { - if (!root) return document; - - if (root instanceof Element) { - return root; - } - - if (root instanceof Document) { - return root; - } - - return root.current; -}; - /** The intersection observer return type */ export interface UseIntersectionObserverReturn { inView: boolean; @@ -87,7 +61,6 @@ export type UseIntersectionObserver = { * const { entry, inView } = useIntersectionObserver(ref); */ export const useIntersectionObserver = ((...params: any[]) => { - const rerender = useRerender(); const target = ( typeof params[0] === 'object' && !('current' in params[0]) ? undefined : params[0] ) as UseIntersectionObserverTarget | undefined; @@ -96,13 +69,13 @@ export const useIntersectionObserver = ((...params: any[]) => { const [entry, setEntry] = useState(); - const internalRef = useRef(); + const [internalRef, setInternalRef] = useState(); const internalOnChangeRef = useRef(); internalOnChangeRef.current = options?.onChange; useEffect(() => { - if (!enabled || !internalRef.current) return; - const element = target ? getTargetElement(target) : internalRef.current; + if (!enabled || !internalRef) return; + const element = target ? getElement(target) : internalRef; if (!element) return; const observer = new IntersectionObserver( @@ -112,32 +85,20 @@ export const useIntersectionObserver = ((...params: any[]) => { }, { ...options, - root: getRootElement(options?.root) + root: options?.root ? (getElement(options?.root) as Element | Document) : document } ); - observer.observe(element); + observer.observe(element as Element); return () => { observer.disconnect(); }; - }, [ - internalRef.current, - target, - options?.rootMargin, - options?.threshold, - options?.root, - enabled - ]); + }, [target, internalRef, options?.rootMargin, options?.threshold, options?.root, enabled]); if (target) return { entry, inView: !!entry?.isIntersecting }; return { - ref: (node: Element) => { - if (!internalRef.current) { - internalRef.current = node; - rerender.update(); - } - }, + ref: setInternalRef, entry, inView: !!entry?.isIntersecting }; diff --git a/src/hooks/useLongPress/useLongPress.demo.tsx b/src/hooks/useLongPress/useLongPress.demo.tsx index d108cbc1..ca7d25e4 100644 --- a/src/hooks/useLongPress/useLongPress.demo.tsx +++ b/src/hooks/useLongPress/useLongPress.demo.tsx @@ -4,7 +4,7 @@ import { useLongPress } from './useLongPress'; const Demo = () => { const counter = useCounter(); - const [longPressedRef, longPressing] = useLongPress(() => counter.inc()); + const [bind, longPressing] = useLongPress(() => counter.inc()); return ( <> @@ -14,7 +14,7 @@ const Demo = () => {

Clicked: {counter.value}

- diff --git a/src/hooks/useLongPress/useLongPress.ts b/src/hooks/useLongPress/useLongPress.ts index dec870dc..49c073e7 100644 --- a/src/hooks/useLongPress/useLongPress.ts +++ b/src/hooks/useLongPress/useLongPress.ts @@ -1,39 +1,39 @@ -import type { RefObject } from 'react'; +import type { MouseEventHandler, RefObject, TouchEventHandler } from 'react'; import { useRef, useState } from 'react'; -import { useEventListener } from '../useEventListener/useEventListener'; - // * The use long press target type */ export type UseLongPressTarget = RefObject | (() => Element) | Element; +export type LongPressReactEvents = + | MouseEventHandler + | TouchEventHandler; + // * The use long press options type */ export interface UseLongPressOptions { // * The threshold time in milliseconds threshold?: number; // * The callback function to be invoked on long press start - onStart?: (event: Event) => void; + onStart?: (event: LongPressReactEvents) => void; // * The callback function to be invoked on long press end - onFinish?: (event: Event) => void; + onFinish?: (event: LongPressReactEvents) => void; // * The callback function to be invoked on long press cancel - onCancel?: (event: Event) => void; + onCancel?: (event: LongPressReactEvents) => void; } -// * The use long press return type */ -export type UseLongPressReturn = [RefObject, boolean]; - -export type UseLongPress = { - ( - target: Target, - callback: (event: Event) => void, - options?: UseLongPressOptions - ): boolean; +// * The use long press bind type */ +export interface UseLongPressBind { + /** The callback function to be invoked on mouse down */ + onMouseDown: MouseEventHandler; + /** The callback function to be invoked on touch start */ + onTouchStart: TouchEventHandler; + /** The callback function to be invoked on mouse up */ + onMouseUp: MouseEventHandler; + /** The callback function to be invoked on touch end */ + onTouchEnd: TouchEventHandler; +} - ( - callback: (event: Event) => void, - options?: UseLongPressOptions, - target?: never - ): UseLongPressReturn; -}; +// * The use long press return type */ +export type UseLongPressReturn = [UseLongPressBind, boolean]; const DEFAULT_THRESHOLD_TIME = 400; @@ -42,20 +42,6 @@ const DEFAULT_THRESHOLD_TIME = 400; * @description - Hook that defines the logic when long pressing an element * @category Sensors * - * @overload - * @template Target The target element - * @param {Target} target The target element to be long pressed - * @param {(event: Event) => void} callback The callback function to be invoked on long press - * @param {number} [options.threshold=400] The threshold time in milliseconds - * @param {(event: Event) => void} [options.onStart] The callback function to be invoked on long press start - * @param {(event: Event) => void} [options.onFinish] The callback function to be invoked on long press finish - * @param {(event: Event) => void} [options.onCancel] The callback function to be invoked on long press cancel - * @returns {void} - * - * @example - * const longPressing = useLongPress(ref, () => console.log('callback')); - * - * @overload * @template Target The target element * @param {Target} target The target element to be long pressed * @param {(event: Event) => void} callback The callback function to be invoked on long press @@ -66,21 +52,17 @@ const DEFAULT_THRESHOLD_TIME = 400; * @returns {UseLongPressReturn} The ref of the target element * * @example - * const [ref, longPressing] = useLongPress(() => console.log('callback')); + * const [bind, longPressing] = useLongPress(() => console.log('callback')); */ -export const useLongPress = ((...params: any[]) => { - const target = ( - params[0] instanceof Function || !('current' in params[0]) ? undefined : params[0] - ) as UseLongPressTarget | undefined; - const callback = (target ? params[1] : params[0]) as (event: Event) => void; - const options = (target ? params[2] : params[1]) as UseLongPressOptions | undefined; - +export const useLongPress = ( + callback: (event: LongPressReactEvents) => void, + options?: UseLongPressOptions +): UseLongPressReturn => { const [isLongPressActive, setIsLongPressActive] = useState(false); - const internalRef = useRef(); const timeoutIdRef = useRef>(); const isPressed = useRef(false); - const start = (event: Event) => { + const start = (event: LongPressReactEvents) => { options?.onStart?.(event); isPressed.current = true; @@ -90,7 +72,7 @@ export const useLongPress = ((...params: any[]) => { }, options?.threshold ?? DEFAULT_THRESHOLD_TIME); }; - const cancel = (event: Event) => { + const cancel = (event: LongPressReactEvents) => { if (isLongPressActive) { options?.onFinish?.(event); } else if (isPressed.current) { @@ -103,11 +85,12 @@ export const useLongPress = ((...params: any[]) => { if (timeoutIdRef.current) clearTimeout(timeoutIdRef.current); }; - useEventListener(target ?? internalRef, 'mousedown', start); - useEventListener(target ?? internalRef, 'touchstart', start); - useEventListener(target ?? internalRef, 'mouseup', cancel); - useEventListener(target ?? internalRef, 'touchend', cancel); + const bind = { + onMouseDown: start, + onTouchStart: start, + onMouseUp: cancel, + onTouchEnd: cancel + } as unknown as UseLongPressBind; - if (target) return isLongPressActive; - return [internalRef, isLongPressActive]; -}) as UseLongPress; + return [bind, isLongPressActive]; +}; diff --git a/src/hooks/useMouse/useMouse.ts b/src/hooks/useMouse/useMouse.ts index 5c73057a..8bb1afd1 100644 --- a/src/hooks/useMouse/useMouse.ts +++ b/src/hooks/useMouse/useMouse.ts @@ -1,7 +1,5 @@ import type { RefObject } from 'react'; -import { useEffect, useRef, useState } from 'react'; - -import { useRerender } from '../useRerender/useRerender'; +import { useEffect, useState } from 'react'; /** The use mouse target element type */ type UseMouseTarget = RefObject | (() => Element) | Element; @@ -62,7 +60,6 @@ export type UseMouse = { * const { ref, x, y, elementX, elementY, elementPositionX, elementPositionY } = useMouse(); */ export const useMouse = ((...params: any[]) => { - const rerender = useRerender(); const target = params[0] as UseMouseTarget | undefined; const [value, setValue] = useState({ @@ -74,12 +71,12 @@ export const useMouse = ((...params: any[]) => { elementPositionY: 0 }); - const internalRef = useRef(); + const [internalRef, setInternalRef] = useState(); useEffect(() => { - if (!target && !internalRef.current) return; + if (!target && !internalRef) return; const onMouseMove = (event: MouseEvent) => { - const element = target ? getElement(target) : internalRef.current; + const element = target ? getElement(target) : internalRef; if (!element) return; const updatedValue = { @@ -109,16 +106,11 @@ export const useMouse = ((...params: any[]) => { return () => { document.removeEventListener('mousemove', onMouseMove); }; - }, [internalRef.current, target]); + }, [internalRef, target]); if (target) return value; return { - ref: (node: Element) => { - if (!internalRef.current) { - internalRef.current = node; - rerender.update(); - } - }, + ref: setInternalRef, ...value }; }) as UseMouse; diff --git a/src/hooks/useResizeObserver/useResizeObserver.ts b/src/hooks/useResizeObserver/useResizeObserver.ts index 262389a4..f451cc70 100644 --- a/src/hooks/useResizeObserver/useResizeObserver.ts +++ b/src/hooks/useResizeObserver/useResizeObserver.ts @@ -1,8 +1,6 @@ import type { RefObject } from 'react'; import { useEffect, useRef, useState } from 'react'; -import { useRerender } from '../useRerender/useRerender'; - /** The resize observer target element type */ type UseResizeObserverTarget = RefObject | (() => Element) | Element; @@ -68,7 +66,6 @@ export type UseResizeObserver = { * const { entries } = useResizeObserver(ref); */ export const useResizeObserver = ((...params: any[]) => { - const rerender = useRerender(); const target = ( typeof params[0] === 'object' && !('current' in params[0]) ? undefined : params[0] ) as UseResizeObserverTarget | UseResizeObserverTarget[] | undefined; @@ -77,12 +74,12 @@ export const useResizeObserver = ((...params: any[]) => { const [entries, setEntries] = useState([]); - const internalRef = useRef(); + const [internalRef, setInternalRef] = useState(); const internalOnChangeRef = useRef(); internalOnChangeRef.current = options?.onChange; useEffect(() => { - if (!enabled) return; + if (!enabled && !target && !internalRef) return; if (Array.isArray(target)) { if (!target.length) return; @@ -102,7 +99,7 @@ export const useResizeObserver = ((...params: any[]) => { }; } - const element = target ? getElement(target) : internalRef.current; + const element = target ? getElement(target) : internalRef; if (!element) return; const observer = new ResizeObserver((entries) => { @@ -114,16 +111,11 @@ export const useResizeObserver = ((...params: any[]) => { return () => { observer.disconnect(); }; - }, [internalRef.current, target, options?.box, enabled]); + }, [internalRef, target, options?.box, enabled]); if (target) return { entries }; return { - ref: (node: Element) => { - if (!internalRef.current) { - internalRef.current = node; - rerender.update(); - } - }, + ref: setInternalRef, entries }; }) as UseResizeObserver; diff --git a/src/utils/helpers/getElement.ts b/src/utils/helpers/getElement.ts new file mode 100644 index 00000000..31ae3606 --- /dev/null +++ b/src/utils/helpers/getElement.ts @@ -0,0 +1,28 @@ +import type { RefObject } from 'react'; + +export type GetElementTarget = + | RefObject + | (() => Element) + | Element + | Window + | Document; + +export const getElement = (target: Target) => { + if (typeof target === 'function') { + return target(); + } + + if (target instanceof Document) { + return target; + } + + if (target instanceof Window) { + return target; + } + + if (target instanceof Element) { + return target; + } + + return target.current; +}; diff --git a/src/utils/helpers/index.ts b/src/utils/helpers/index.ts index 346caf9c..59fc4a29 100644 --- a/src/utils/helpers/index.ts +++ b/src/utils/helpers/index.ts @@ -1,4 +1,5 @@ export * from './debounce'; +export * from './getElement'; export * from './getRetry'; export * from './isClient'; export * from './isPermissionAllowed';