Шпаргалка по полезным хукам для React-разработчика
Шпаргалка, включающая набор пользовательских хуков, которые могут оказаться полезными при создании интерфейсов на React.js. Склонение слов, работа с мета-тегами, управление модальными окнами, троттлинг и отложение выполнение, и многое другое.
Хук для взаимодействия с клавиатурой
import { useEffect } from 'react'
const useKeyListener = (callback) => {
useEffect(() => {
const listener = (e) => {
e = e || window.event
const tagName = e.target.localName || e.target.tagName
// Принимаем только события на уровне body,
// чтобы избежать их перехвата, например, в полях ввода
if (tagName.toUpperCase() === 'BODY') {
callback(e)
}
}
document.addEventListener('keydown', listener, true)
return () => {
document.removeEventListener('keydown', listener, true)
}
}, [callback])
}
export default useKeyListener
Хук для склонения слов
Хук, который в зависимости от числа подставляет необходимое склонение слова или выражения.
Пример использования:
const { text } = useWordDeclination(correctAnswersCount, [
"верный ответ",
"верных ответа",
"верных ответов",
])
Реализация (TypeScript):
import { useMemo } from "react"
export const useWordDeclination = (
n: number,
strings: [string, string, string],
) => {
const text = useMemo(() => {
const words = [strings[0], strings[1], strings[2]]
return words[
n % 100 > 4 && n % 100 < 20
? 2
: [2, 0, 1, 1, 1, 2][n % 10 < 5 ? n % 10 : 5]
]
}, [n, strings])
return { text }
}
Хук для изменения мета-тегов
Хук, изменяющий title и description мета-тэги для страницы при её открытии и восстанавливающий их значения при ее покидании.
Пример использования:
usePageMeta({ title, description })
Реализация:
import { useEffect } from "react"
type Props = {
title: string
description?: string
}
const DEFAULT = {
title: "Заголовок страницы по умолчанию (заголовок для главной страницы)",
description: "Описание по умолчанию (описание для главной страницы)",
}
export const usePageMeta = ({ title, description }: Props) => {
useEffect(() => {
document.title = title
description &&
document
.querySelector('head meta[name="description"]')
?.setAttribute("content", description)
return () => {
document.title = DEFAULT.title
document
.querySelector('head meta[name="description"]')
?.setAttribute("content", DEFAULT.description)
}
}, [title, description])
}
Хук для управления модальным окном
export function useModal() {
const [isOpened, setIsOpened] = useState(false);
const restrictBodyScroll = () => {
document.body.style.height = "100vh";
document.body.style.overflow = "hidden";
};
const allowBodyScroll = () => {
document.body.style.height = "";
document.body.style.overflow = "";
};
const open = () => {
setTimeout(() => {
restrictBodyScroll();
}, 0);
setIsOpened(true);
};
const close = () => {
allowBodyScroll();
setIsOpened(false);
};
useEffect(() => {
return close;
}, []);
return {
isOpened,
open,
close,
};
}
setTimeout
в open
нужен для того, чтобы скролл body
корректно запретился в том случае, если модалка открывается сразу после закрытия предыдущей.
Пример использования:
const SomeComponent = () => {
const confirmModal = useModal();
return (
<div>
<button onClick={confirmModal.open}>Открыть модальное окно</button>
<SomeModalComponent
isOpened={confirmModal.isOpened}
close={confirmModal.close}
/>
</div>
);
};
export default SomeComponent;
Хук для получения размеров окна
import { useState, useEffect } from "react";
interface WindowSize {
width: number;
height: number;
}
const useWindowSize = (): WindowSize => {
const [windowSize, setWindowSize] = useState<WindowSize>({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return windowSize;
};
Хук для setInterval
import { useState, useEffect, useRef } from "react";
const useInterval = (callback: () => void, delay: number | null) => {
const savedCallback = useRef<() => void>();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
function tick() {
savedCallback.current && savedCallback.current();
}
if (delay !== null && delay > 0) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
} else {
tick();
}
}, [delay]);
};
Хук для доступа к предыдущему значению стейта
import { useRef, useEffect } from "react";
const usePrevious = <T>(value: T): T | undefined => {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
export default usePrevious;
Пример использования:
import React, { useState } from "react";
import usePrevious from "./usePrevious";
const Counter = () => {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
const handleClick = () => {
setCount(count => count + 1);
};
return (
<div>
Current count: {count}, Previous count: {prevCount}
<button onClick={handleClick}>Increment</button>
</div>
);
};
export default Counter;
Хук для троттлинга выполнения
import { useEffect, useRef, useState } from 'react'
function useThrottle<T>(value: T, interval = 500): T {
const [throttledValue, setThrottledValue] = useState<T>(value)
const lastExecuted = useRef<number>(Date.now())
useEffect(() => {
if (Date.now() >= lastExecuted.current + interval) {
lastExecuted.current = Date.now()
setThrottledValue(value)
} else {
const timerId = setTimeout(() => {
lastExecuted.current = Date.now()
setThrottledValue(value)
}, interval)
return () => clearTimeout(timerId)
}
}, [value, interval])
return throttledValue
}
Пример использования:
import React, { useEffect, useState } from 'react'
import { useThrottle } from './useThrottle'
export default function App() {
const [value, setValue] = useState('hello')
const throttledValue = useThrottle(value)
useEffect(() => console.log(`throttledValue changed: ${throttledValue}`), [
throttledValue,
])
function onChange(event: React.ChangeEvent<HTMLInputElement>) {
setValue(event.target.value)
}
return (
<div>
Input: <input value={value} onChange={onChange} />
<p>Throttled value: {throttledValue}</p>
</div>
)
}
Хук для отложенного выполнения
import React from "react";
function useDebounce(value: string, delay: number = 500) {
const [debouncedValue, setDebouncedValue] = React.useState(value);
React.useEffect(() => {
const handler: NodeJS.Timeout = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
Хук для работы с Intersection Observer
import { useEffect, useRef } from 'react';
export type OnIntersect = (elt: HTMLElement) => void;
export const useIntersectionObserver = (
onIntersect: OnIntersect,
{
threshold = 0,
root = null,
rootMargin = undefined,
}: IntersectionObserverInit = {}
) => {
const ref = useRef<HTMLElement>();
let observer: IntersectionObserver;
useEffect(() => {
if (window.IntersectionObserver) {
const callback: IntersectionObserverCallback = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
onIntersect(entry.target as HTMLElement);
observer.unobserve(entry.target);
}
});
};
const args = { threshold, root, rootMargin };
observer = new IntersectionObserver(callback, args);
observer.observe(ref.current);
return () => observer.disconnect();
}
}, [onIntersect, JSON.stringify(threshold), root, rootMargin]);
return ref;
};
Пример использования:
import React from 'react';
const onObserveSetAnimate = (elt: HTMLElement) => elt.classList.add('animate');
const Foo = () => {
const ref = useIntersectionObserver(onObserveSetAnimate);
return <div ref={ref}>hello, world</div>;
}
Хук для получения информации о сетевом соединении
import { useEffect, useState } from "react";
interface NetworkInformation extends EventTarget {
downlink?: number;
effectiveType?: "slow-2g" | "2g" | "3g" | "4g";
rtt?: number;
saveData?: boolean;
onchange?: EventListener;
}
interface NavigatorWithNetworkInformation extends Navigator {
connection?: NetworkInformation;
}
interface NetworkState {
downlink?: number | null;
effectiveType?: "slow-2g" | "2g" | "3g" | "4g" | null;
rtt?: number | null;
saveData?: boolean | null;
isOnline: boolean;
}
function useNetwork(): NetworkState {
const [networkState, setNetworkState] = useState<NetworkState>({
downlink: null,
effectiveType: null,
rtt: null,
saveData: null,
isOnline: false, // Default to false; updated correctly on the client-side
});
useEffect(() => {
// Ensure we are in the browser environment
if (typeof window === "undefined") {
return;
}
const nav = navigator as NavigatorWithNetworkInformation;
if (!nav.connection) {
setNetworkState((prevState) => ({
...prevState,
downlink: null,
effectiveType: null,
rtt: null,
saveData: null,
isOnline: navigator.onLine, // Update online status in the browser
}));
return;
}
const updateNetworkInfo = () => {
const { downlink, effectiveType, rtt, saveData } = nav.connection!;
setNetworkState((prevState) => ({
...prevState,
downlink,
effectiveType,
rtt,
saveData,
isOnline: navigator.onLine,
}));
};
updateNetworkInfo();
nav.connection!.addEventListener("change", updateNetworkInfo);
window.addEventListener("online", () =>
setNetworkState((prevState) => ({ ...prevState, isOnline: true })),
);
window.addEventListener("offline", () =>
setNetworkState((prevState) => ({ ...prevState, isOnline: false })),
);
return () => {
if (nav.connection) {
nav.connection.removeEventListener("change", updateNetworkInfo);
}
window.removeEventListener("online", () =>
setNetworkState((prevState) => ({ ...prevState, isOnline: true })),
);
window.removeEventListener("offline", () =>
setNetworkState((prevState) => ({ ...prevState, isOnline: false })),
);
};
}, []);
return networkState;
}
export default useNetwork;
Хук для информации о геолокации в реальном времени
import { useEffect, useState } from "react";
interface LocationOptions {
enableHighAccuracy?: boolean;
timeout?: number;
maximumAge?: number;
}
interface LocationState {
coords: {
latitude: number | null;
longitude: number | null;
accuracy: number | null;
altitude: number | null;
altitudeAccuracy: number | null;
heading: number | null;
speed: number | null;
};
locatedAt: number | null;
error: string | null;
}
const useLocation = (options: LocationOptions = {}) => {
const [location, setLocation] = useState<LocationState>({
coords: {
latitude: null,
longitude: null,
accuracy: null,
altitude: null,
altitudeAccuracy: null,
heading: null,
speed: null,
},
locatedAt: null,
error: null,
});
useEffect(() => {
// Ensuring this runs only in a client-side environment
if (typeof window === "undefined" || !("geolocation" in navigator)) {
setLocation((prevState) => ({
...prevState,
error:
"Geolocation is not supported by your browser or not available in the current environment",
}));
return;
}
const handleSuccess = (position: GeolocationPosition) => {
setLocation({
coords: {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
altitude: position.coords.altitude,
altitudeAccuracy: position.coords.altitudeAccuracy,
heading: position.coords.heading,
speed: position.coords.speed,
},
locatedAt: position.timestamp,
error: null,
});
};
const handleError = (error: GeolocationPositionError) => {
setLocation((prevState) => ({
...prevState,
error: error.message,
}));
};
const geoOptions = {
enableHighAccuracy: options.enableHighAccuracy || false,
timeout: options.timeout || Infinity,
maximumAge: options.maximumAge || 0,
};
const watcher = navigator.geolocation.watchPosition(
handleSuccess,
handleError,
geoOptions,
);
return () => navigator.geolocation.clearWatch(watcher);
}, [options.enableHighAccuracy, options.timeout, options.maximumAge]);
return location;
};
export default useLocation;
Хук для преобразования голоса в текст
import { useCallback, useEffect, useState } from "react";
// Define custom types for SpeechRecognition and SpeechRecognitionEvent
interface ISpeechRecognitionEvent extends Event {
results: SpeechRecognitionResultList;
resultIndex: number;
}
interface ISpeechRecognition extends EventTarget {
lang: string;
continuous: boolean;
interimResults: boolean;
maxAlternatives: number;
start: () => void;
stop: () => void;
onresult: (event: ISpeechRecognitionEvent) => void;
onerror: (event: Event) => void;
onend: () => void;
}
declare global {
interface Window {
SpeechRecognition: new () => ISpeechRecognition;
webkitSpeechRecognition: new () => ISpeechRecognition;
}
}
interface UseSpeechToTextProps {
lang?: string;
continuous?: boolean;
interimResults?: boolean;
maxAlternatives?: number;
onResult?: (result: string) => void;
onError?: (error: string) => void;
}
export const useSpeechToText = ({
lang = "en-US",
continuous = true,
interimResults = true,
maxAlternatives = 1,
onResult,
onError,
}: UseSpeechToTextProps = {}) => {
const [isListening, setIsListening] = useState(false);
const [transcript, setTranscript] = useState("");
const [lastProcessedIndex, setLastProcessedIndex] = useState(0);
const recognition: ISpeechRecognition | null =
typeof window !== "undefined" &&
(window.SpeechRecognition || window.webkitSpeechRecognition)
? new (window.SpeechRecognition || window.webkitSpeechRecognition)()
: null;
const handleResult = useCallback(
(event: ISpeechRecognitionEvent) => {
let interimTranscript = "";
let finalTranscript = "";
// Iterate through all the current results
for (let i = lastProcessedIndex; i < event.results.length; i++) {
const result = event.results[i];
// If the result is final, append to the final transcript
if (result.isFinal) {
finalTranscript += result[0].transcript + " ";
setLastProcessedIndex(i + 1);
} else {
// Otherwise, append to the interim transcript
interimTranscript += result[0].transcript + " ";
}
}
// Update the transcript state with a combination of the final and interim results
setTranscript(transcript + finalTranscript + interimTranscript);
// Invoke callback with the latest transcript
onResult && onResult(transcript + finalTranscript + interimTranscript);
},
[onResult, transcript, lastProcessedIndex],
);
// start and stop functions using useCallback
const start = useCallback(() => {
if (!recognition || isListening) return;
setTranscript("");
setLastProcessedIndex(0);
setIsListening(true);
recognition.start();
}, [recognition, isListening]);
const stop = useCallback(() => {
if (!recognition || !isListening) return;
recognition.stop();
setIsListening(false);
}, [recognition, isListening]);
useEffect(() => {
if (!recognition) {
onError &&
onError("Speech recognition is not supported in this browser.");
return;
}
recognition.lang = lang;
recognition.continuous = continuous;
recognition.interimResults = interimResults;
recognition.maxAlternatives = maxAlternatives;
recognition.onresult = handleResult;
recognition.onerror = (event) => onError && onError(event.type);
recognition.onend = () => {
setIsListening(false);
};
return () => {
if (isListening) recognition.stop();
};
}, [
lang,
continuous,
interimResults,
maxAlternatives,
handleResult,
onError,
recognition,
start,
stop,
isListening,
]);
return { start, stop, transcript, isListening };
};
export default useSpeechToText;
Хук для продвинутых жестов на тачпадах
import { useEffect, useRef } from "react";
type GestureType =
| "swipeUp"
| "swipeDown"
| "swipeLeft"
| "swipeRight"
| "tap"
| "pinch"
| "zoom";
interface GestureConfig {
gesture: GestureType;
touchCount: number;
callback: () => void;
elementRef?: React.RefObject<HTMLElement>;
}
const useGesture = (config: GestureConfig) => {
// Use a function to lazily get the target element in a client-side environment
const getTargetElement = () => {
if (typeof window !== "undefined") {
return config.elementRef?.current || document;
}
// Return a dummy object for SSR
return {
addEventListener: () => {},
removeEventListener: () => {},
};
};
const targetElement = getTargetElement();
const gestureStateRef = useRef({
touchStartX: 0,
touchStartY: 0,
touchEndX: 0,
touchEndY: 0,
touchTime: 0,
initialDistance: 0,
finalDistance: 0,
gestureTriggered: false,
});
useEffect(() => {
const onTouchStart = (e: Event) => {
const touchEvent = e as TouchEvent;
if (touchEvent.touches.length === config.touchCount) {
e.preventDefault();
const touch = touchEvent.touches[0];
if (!touch) return;
gestureStateRef.current = {
...gestureStateRef.current,
touchStartX: touch.clientX,
touchStartY: touch.clientY,
touchTime: Date.now(),
gestureTriggered: false,
};
if (config.gesture === "pinch" || config.gesture === "zoom") {
const touch2 = touchEvent.touches[1];
if (!touch2) return;
gestureStateRef.current.initialDistance = Math.hypot(
touch2.clientX - touch.clientX,
touch2.clientY - touch.clientY,
);
}
}
};
const onTouchMove = (e: Event) => {
const touchEvent = e as TouchEvent;
if (
touchEvent.touches.length === config.touchCount &&
!gestureStateRef.current.gestureTriggered
) {
e.preventDefault();
const touch = touchEvent.touches[0];
if (!touch) return;
gestureStateRef.current.touchEndX = touch.clientX;
gestureStateRef.current.touchEndY = touch.clientY;
if (config.gesture === "pinch" || config.gesture === "zoom") {
const touch2 = touchEvent.touches[1];
if (!touch2) return;
gestureStateRef.current.finalDistance = Math.hypot(
touch2.clientX - touch.clientX,
touch2.clientY - touch.clientY,
);
}
}
};
const triggerGesture = () => {
config.callback();
};
const onTouchEnd = () => {
handleGesture();
};
const handleGesture = () => {
if (gestureStateRef.current.gestureTriggered) return;
const {
touchStartX,
touchStartY,
touchEndX,
touchEndY,
touchTime,
initialDistance,
finalDistance,
} = gestureStateRef.current;
const dx = touchEndX - touchStartX;
const dy = touchEndY - touchStartY;
const timeDiff = Date.now() - touchTime;
const distance = Math.hypot(dx, dy);
switch (config.gesture) {
case "swipeUp":
if (dy < -50 && Math.abs(dx) < 50) triggerGesture();
break;
case "swipeDown":
if (dy > 50 && Math.abs(dx) < 50) triggerGesture();
break;
case "swipeLeft":
if (dx < -50 && Math.abs(dy) < 50) triggerGesture();
break;
case "swipeRight":
if (dx > 50 && Math.abs(dy) < 50) triggerGesture();
break;
case "tap":
if (distance < 30 && timeDiff < 200) triggerGesture();
break;
case "pinch":
if (finalDistance < initialDistance) triggerGesture();
break;
case "zoom":
if (finalDistance > initialDistance) triggerGesture();
break;
}
gestureStateRef.current.gestureTriggered = true;
};
// Only attach event listeners in the browser environment
if (typeof window !== "undefined" || typeof document !== "undefined") {
targetElement.addEventListener("touchstart", onTouchStart, {
passive: false,
});
targetElement.addEventListener("touchmove", onTouchMove, {
passive: false,
});
targetElement.addEventListener("touchend", onTouchEnd, {
passive: false,
});
return () => {
targetElement.removeEventListener("touchstart", onTouchStart);
targetElement.removeEventListener("touchmove", onTouchMove);
targetElement.removeEventListener("touchend", onTouchEnd);
};
}
}, [config, targetElement]);
return gestureStateRef.current;
};
export default useGesture;
Хук для отслеживания выделения текста
import { useEffect, useState } from "react";
type UseTextSelectionReturn = {
text: string;
rects: DOMRect[];
ranges: Range[];
selection: Selection | null;
};
const getRangesFromSelection = (selection: Selection): Range[] => {
const rangeCount = selection.rangeCount;
return Array.from({ length: rangeCount }, (_, i) => selection.getRangeAt(i));
};
const isSelectionInsideRef = (
selection: Selection,
ref: React.RefObject<HTMLElement>,
) => {
if (!ref.current || selection.rangeCount === 0) return true;
const range = selection.getRangeAt(0);
return ref.current.contains(range.commonAncestorContainer);
};
export function useTextSelection(
elementRef?: React.RefObject<HTMLElement>,
): UseTextSelectionReturn {
const [selection, setSelection] = useState<Selection | null>(null);
const [text, setText] = useState("");
const [ranges, setRanges] = useState<Range[]>([]);
const [rects, setRects] = useState<DOMRect[]>([]);
useEffect(() => {
const onSelectionChange = () => {
const newSelection = window.getSelection();
if (
newSelection &&
(!elementRef || isSelectionInsideRef(newSelection, elementRef))
) {
setSelection(newSelection);
setText(newSelection.toString());
const newRanges = getRangesFromSelection(newSelection);
setRanges(newRanges);
setRects(newRanges.map((range) => range.getBoundingClientRect()));
} else {
setText("");
setRanges([]);
setRects([]);
setSelection(null);
}
};
document.addEventListener("selectionchange", onSelectionChange);
return () => {
document.removeEventListener("selectionchange", onSelectionChange);
};
}, [elementRef]);
return {
text,
rects,
ranges,
selection,
};
}
Хук для взаимодействия между вкладками
import { useCallback, useEffect, useRef, useState } from "react";
interface UseBroadcastChannelOptions {
name: string;
onMessage?: (event: MessageEvent) => void;
onMessageError?: (event: MessageEvent) => void;
}
interface UseBroadcastChannelReturn<D, P> {
isSupported: boolean;
channel: BroadcastChannel | undefined;
data: D | undefined;
post: (data: P) => void;
close: () => void;
messageError: Event | undefined;
isClosed: boolean;
}
function useBroadcastChannel<D, P>(
options: UseBroadcastChannelOptions
): UseBroadcastChannelReturn<D, P> {
const [isSupported, setIsSupported] = useState<boolean>(false);
const channelRef = useRef<BroadcastChannel | undefined>(undefined);
const [data, setData] = useState<D | undefined>();
const [messageError, setMessageError] = useState<Event | undefined>(
undefined
);
const [isClosed, setIsClosed] = useState<boolean>(false);
useEffect(() => {
setIsSupported(
typeof window !== "undefined" && "BroadcastChannel" in window
);
}, []);
const handleMessage = useCallback(
(event: MessageEvent) => {
setData(event.data as D);
options.onMessage?.(event);
},
[options.onMessage]
);
const handleMessageError = useCallback(
(event: MessageEvent) => {
setMessageError(event);
options.onMessageError?.(event);
},
[options.onMessageError]
);
useEffect(() => {
if (isSupported) {
const newChannel = new BroadcastChannel(options.name);
channelRef.current = newChannel;
newChannel.addEventListener("message", handleMessage);
newChannel.addEventListener("messageerror", handleMessageError);
return () => {
newChannel.removeEventListener("message", handleMessage);
newChannel.removeEventListener("messageerror", handleMessageError);
if (!isClosed) {
newChannel.close();
}
channelRef.current = undefined;
};
}
}, [isSupported, options.name, handleMessage, handleMessageError]);
const post = useCallback(
(messageData: P) => {
if (channelRef.current && !isClosed) {
channelRef.current.postMessage(messageData);
}
},
[isClosed]
);
const close = useCallback(() => {
if (channelRef.current && !isClosed) {
channelRef.current.close();
setIsClosed(true);
}
}, [isClosed]);
return {
isSupported,
channel: channelRef.current,
data,
post,
close,
messageError,
isClosed,
};
}
export default useBroadcastChannel;
Хук для работы с Cookie
import { useEffect, useState } from "react";
type UseCookieReturnType = {
cookie: string | undefined;
setCookie: (value: string, days?: number) => void;
};
const useCookie = (cookieName: string): UseCookieReturnType => {
const getCookie = (name: string): string | undefined => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(";").shift();
};
const setCookie = (name: string, value: string, days?: number): void => {
let expires = "";
if (days) {
const date = new Date();
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
expires = `; expires=${date.toUTCString()}`;
}
document.cookie = `${name}=${value}${expires}; path=/`;
};
const [cookie, setCookieState] = useState<string | undefined>(() =>
getCookie(cookieName)
);
useEffect(() => {
const intervalId = window.setInterval(() => {
const newCookie = getCookie(cookieName);
if (newCookie !== cookie) setCookieState(newCookie);
}, 100);
return () => {
window.clearInterval(intervalId);
};
}, [cookie, cookieName]);
return {
cookie,
setCookie: (value: string, days?: number) =>
setCookie(cookieName, value, days),
};
};
export default useCookie;
Хук для отслеживания размеров элемента
import { useEffect, useRef, useState } from "react";
interface MeasureResult<T extends Element> {
ref: React.RefObject<T>;
bounds: DOMRectReadOnly;
}
const useMeasure = <T extends Element = Element>(): MeasureResult<T> => {
const ref = useRef<T>(null);
const [bounds, setBounds] = useState<DOMRectReadOnly>(new DOMRectReadOnly());
useEffect(() => {
let observer: ResizeObserver;
if (ref.current) {
observer = new ResizeObserver(([entry]) => {
if (entry) {
setBounds(entry.contentRect);
}
});
observer.observe(ref.current);
}
return () => {
if (observer) {
observer.disconnect();
}
};
}, []);
return { ref, bounds };
};
export default useMeasure;
Хук для работы с localStorage
import { useEffect, useState } from "react";
function useLocalStorage() {
const [loadingStates, setLoadingStates] = useState<Map<string, boolean>>(
new Map()
);
const setStorageValue = <T>(key: string, value: T) => {
try {
window.localStorage.setItem(key, JSON.stringify(value));
window.dispatchEvent(new Event("storage"));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
};
const getStorageValue = <T>(
key: string,
fallbackValue?: T
): [T | undefined, boolean] => {
const [value, setStorageValue] = useState<T | undefined>(fallbackValue);
const [isLoading, setIsLoading] = useState<boolean>(true);
useEffect(() => {
setIsLoading(loadingStates.get(key) ?? true);
try {
const item = window.localStorage.getItem(key);
setStorageValue(item !== null ? JSON.parse(item) : fallbackValue);
} catch (error) {
console.error(error);
setStorageValue(fallbackValue);
} finally {
setIsLoading(false);
setLoadingStates((prev) => new Map(prev).set(key, false));
}
}, [key, fallbackValue, loadingStates]);
return [value, isLoading];
};
// Effect to update component when localStorage changes
useEffect(() => {
const handleStorageChange = (event: StorageEvent) => {
if (event.key) {
setLoadingStates((prev) =>
new Map(prev).set(event.key as string, true)
);
}
};
window.addEventListener("storage", handleStorageChange);
return () => {
window.removeEventListener("storage", handleStorageChange);
};
}, []);
return { getStorageValue, setStorageValue };
}
export default useLocalStorage;
Хук для отслеживания активности пользователя
import { useEffect, useState } from "react";
const defaultEvents = [
"mousemove",
"mousedown",
"touchstart",
"keydown",
"wheel",
"resize",
];
interface UseIdleOptions {
timeout?: number;
events?: string[];
}
const useIdle = ({
timeout = 5000,
events = defaultEvents,
}: UseIdleOptions = {}) => {
const [isIdle, setIsIdle] = useState<boolean>(false);
useEffect(() => {
let timer: ReturnType<typeof setTimeout>;
const resetTimer = () => {
clearTimeout(timer);
setIsIdle(false);
timer = setTimeout(() => setIsIdle(true), timeout);
};
// Initialize the timer
resetTimer();
// Event handler to reset the timer on user activity
events.forEach((event) => window.addEventListener(event, resetTimer));
// Cleanup function
return () => {
clearTimeout(timer);
events.forEach((event) => window.removeEventListener(event, resetTimer));
};
}, [timeout, events]);
return isIdle;
};
export default useIdle;
Хук для отслеживания долгого нажатия
import { useCallback, useEffect, useRef } from "react";
export function useLongPress({
delay,
onLongPress,
onClick,
onCancel,
onFinish,
onStart,
}: {
delay: number;
onLongPress: () => void;
onClick?: () => void;
onCancel?: () => void;
onFinish?: () => void;
onStart?: () => void;
}) {
const timerRef = useRef<ReturnType<typeof setTimeout>>();
const pressTriggeredRef = useRef<boolean>(false);
const pressInitiatedRef = useRef<boolean>(false);
const start = useCallback(
(event: React.MouseEvent<any, MouseEvent> | React.TouchEvent<any>) => {
// Only left clicks (button 0)
if ("button" in event && event.button !== 0) return;
pressTriggeredRef.current = false;
pressInitiatedRef.current = true;
if (onStart) onStart();
timerRef.current = setTimeout(() => {
if (pressInitiatedRef.current) {
onLongPress();
if (onFinish) onFinish();
pressTriggeredRef.current = true;
}
}, delay);
},
[onLongPress, delay, onFinish, onStart]
);
const clear = useCallback(() => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
if (!pressTriggeredRef.current && pressInitiatedRef.current && onClick) {
onClick();
}
pressInitiatedRef.current = false;
timerRef.current = undefined;
if (!pressTriggeredRef.current) {
if (onCancel) onCancel();
}
}, [onClick, onCancel]);
useEffect(() => {
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, []);
return {
onMouseDown: start,
onTouchStart: start,
onMouseUp: clear,
onMouseLeave: clear,
onTouchEnd: clear,
};
}
Хук для копирования в буффер обмена
import { useCallback, useState } from "react";
// Custom hook to copy text to clipboard
const useCopyToClipboard = (timeoutDuration: number = 1000) => {
const [copied, setCopied] = useState(false);
const [error, setError] = useState<Error | null>(null);
const copyToClipboard = useCallback(
async (text: string) => {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setError(null);
setTimeout(() => setCopied(false), timeoutDuration);
} catch (err) {
setError(err instanceof Error ? err : new Error("Failed to copy text"));
}
},
[timeoutDuration]
);
return { copied, error, copyToClipboard };
};
export default useCopyToClipboard;
Хук для динамической высоты textarea
import { useEffect } from "react";
const useDynamicTextareaSize = (
textareaRef: React.RefObject<HTMLTextAreaElement>,
textContent: string,
// optional maximum height after which textarea becomes scrollable
maxHeight?: number
): void => {
useEffect(() => {
const currentTextarea = textareaRef.current;
if (currentTextarea) {
// Temporarily collapse the textarea to calculate the required height
currentTextarea.style.height = "0px";
const contentHeight = currentTextarea.scrollHeight;
if (maxHeight) {
// Set max-height and adjust overflow behavior if maxHeight is provided
currentTextarea.style.maxHeight = `${maxHeight}px`;
currentTextarea.style.overflowY = contentHeight > maxHeight ? "scroll" : "hidden";
currentTextarea.style.height = `${Math.min(contentHeight, maxHeight)}px`;
} else {
// Adjust height without max height constraint
currentTextarea.style.height = `${contentHeight}px`;
}
}
}, [textareaRef, textContent, maxHeight]);
};
export default useDynamicTextareaSize;
Хук для работы с Web Share API
import { useCallback, useEffect, useState } from "react";
// Types for the useShare hook parameters
interface UseShareParams {
onShare?: (content: ShareParams) => void;
onSuccess?: (content: ShareParams) => void;
onError?: (error: any) => void;
fallback?: () => void;
successTimeout?: number;
}
// Types for the share function parameters
interface ShareParams {
title?: string;
text?: string;
url?: string;
}
const useShare = ({
onShare,
onSuccess,
onError,
fallback,
successTimeout = 3000,
}: UseShareParams) => {
const [isSupported, setIsSupported] = useState<boolean>(false);
const [isReady, setIsReady] = useState<boolean>(false);
const [isShared, setIsShared] = useState<boolean>(false);
// Check for Web Share API support
useEffect(() => {
if (typeof window !== "undefined" && "navigator" in window) {
setIsSupported("share" in navigator);
setIsReady(true);
}
}, []);
// Function to handle the reset of isShared state
const resetIsShared = (timeout: number) => {
const timer = setTimeout(() => setIsShared(false), timeout);
return () => clearTimeout(timer);
};
// Function to handle sharing or fallback
const share = useCallback(
async (content: ShareParams) => {
if (isSupported) {
// Execute onShare callback if provided
onShare?.(content);
try {
// Attempt to use the Web Share API
await navigator.share(content);
setIsShared(true);
// Execute onSuccess callback if provided
onSuccess?.(content);
// Reset isShared after the user-defined or default period of time
return resetIsShared(successTimeout);
} catch (error) {
// Execute onError callback if provided
onError?.(error);
}
} else {
// Execute fallback function if provided
fallback?.();
setIsShared(true);
// Reset isShared after the user-defined or default period of time
return resetIsShared(successTimeout);
}
},
[fallback, isSupported, onError, onShare, onSuccess, successTimeout],
);
return { share, isSupported, isReady, isShared };
};
export default useShare;