diff --git a/src/shared/hooks/index.ts b/src/shared/hooks/index.ts new file mode 100644 index 0000000..c114938 --- /dev/null +++ b/src/shared/hooks/index.ts @@ -0,0 +1 @@ +export { default as useCombinedRefs } from './useCombineRef'; diff --git a/src/shared/hooks/useCombineRef.tsx b/src/shared/hooks/useCombineRef.tsx new file mode 100644 index 0000000..13e0f3e --- /dev/null +++ b/src/shared/hooks/useCombineRef.tsx @@ -0,0 +1,55 @@ +/** + * A combined ref implementation using the callback ref cleanups feature. + * Note that this won't work yet as callback ref cleanups feature isn't released yet and thought to be released in React 19 + */ + +import { useCallback } from 'react'; + +import type { ForwardedRef } from 'react'; + +type OptionalRef = ForwardedRef | undefined; + +type Cleanup = (() => void) | undefined; + +const setRef = (ref: OptionalRef, value: T): Cleanup => { + if (typeof ref === 'function') { + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression + const cleanup = ref(value); + + if (typeof cleanup === 'function') { + return cleanup; + } + + // eslint-disable-next-line @stylistic/max-statements-per-line + return () => { ref(null); }; + } + + if (ref) { + // eslint-disable-next-line no-param-reassign + ref.current = value; + + // eslint-disable-next-line no-param-reassign,no-return-assign + return () => ref.current = null; + } + + // eslint-disable-next-line no-undefined + return undefined; +}; + +const useCombinedRefs = (...refs: OptionalRef[]) => useCallback((value: T | null) => { + const cleanups: Cleanup[] = []; + + for (const ref of refs) { + const cleanup = setRef(ref, value); + cleanups.push(cleanup); + } + + return () => { + for (const cleanup of cleanups) { + cleanup?.(); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps +}, refs); + +export default useCombinedRefs;