2065 просмотров
от 17 февраля 2024
React

Шпаргалка по полезным хукам для React-разработчика

Шпаргалка, включающая набор пользовательских хуков, которые могут оказаться полезными при создании интерфейсов на React.js. Склонение слов, работа с мета-тегами, управление модальными окнами, троттлинг и отложение выполнение, и многое другое.

1

Хук для взаимодействия с клавиатурой

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

Комментарии
0/3000
2

Хук для склонения слов

Хук, который в зависимости от числа подставляет необходимое склонение слова или выражения. Пример использования: 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 } }

Комментарии
0/3000
3

Хук для изменения мета-тегов

Хук, изменяющий 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]) }

Комментарии
0/3000
4

Хук для управления модальным окном

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;

Комментарии
0/3000
5

Хук для получения размеров окна

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; };

Комментарии
0/3000
6

Хук для 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]); };

Комментарии
0/3000
7

Хук для доступа к предыдущему значению стейта

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;

Комментарии
0/3000
8

Хук для троттлинга выполнения

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> ) }

Комментарии
0/3000
9

Хук для отложенного выполнения

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; }

Комментарии
0/3000
10

Хук для работы с 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>; }

Комментарии
0/3000
11

Хук для получения информации о сетевом соединении

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;

Комментарии
0/3000
12

Хук для информации о геолокации в реальном времени

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;

Комментарии
0/3000
13

Хук для преобразования голоса в текст

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;

Комментарии
0/3000
14

Хук для продвинутых жестов на тачпадах

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;

Комментарии
0/3000
15

Хук для отслеживания выделения текста

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, }; }

Комментарии
0/3000
16

Хук для взаимодействия между вкладками

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;

Комментарии
0/3000
17

Хук для работы с 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;

Комментарии
0/3000
18

Хук для отслеживания размеров элемента

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;

Комментарии
0/3000
19

Хук для работы с 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;

Комментарии
0/3000
20

Хук для отслеживания активности пользователя

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;

Комментарии
0/3000
21

Хук для отслеживания долгого нажатия

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, }; }

Комментарии
0/3000
22

Хук для копирования в буффер обмена

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;

Комментарии
0/3000
23

Хук для динамической высоты 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;

Комментарии
0/3000
24

Хук для работы с 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;

Комментарии
0/3000
Вопросники по React
React
25 вопросов
Вопросы и ответы с собеседований по React.js
3081 просмотр
React
24 вопроса
Шпаргалка по полезным хукам для React-разработчика
2065 просмотров
Хотите заработать на создании вопросников?
Стать редактором
Смежные категории
TypeScript
21 вопрос
Вопросы и ответы с собеседований по TypeScript
3246 просмотров
CSS
19 вопросов
Вопросы и ответы с собеседований по CSS
2100 просмотров
JavaScript
58 вопросов
Вопросы и ответы с собеседований по JavaScript
6148 просмотров
Angular
74 вопроса
Вопросы и ответы с собеседований по Angular
1683 просмотра
Vue
15 вопросов
Вопросы и ответы с собеседований по Vue.js
1729 просмотров
Рекомендуем
Computer Science
28 вопросов
Объяснение паттернов проектирования с примерами
1424 просмотра
Computer Science
13 вопросов
Вопросы и ответы с собеседований про ООП
1231 просмотр
Базы данных
60 вопросов
Вопросы и ответы с собеседований по SQL
2377 просмотров
Computer Science
11 вопросов
Вопросы и ответы про интернет-протоколы
1464 просмотра
Computer Science
12 вопросов
Вопросы с собеседований про операционные системы
1061 просмотр
Git
20 вопросов
Вопросы и ответы с собеседований по Git
1784 просмотра
Другие разделы

Лента

Активность пользователей Девстанции

Перейти к ленте

Лидеры

Рейтинг самых результативных пользователей сообщества

Перейти к лидерам

Треды

Общение по интересам и связь с разработчиками

Перейти к тредам

Задачи

Решение алгоритмических задач с собеседований

Перейти к задачам

Вопросы

Ответы на вопросы с технических собеседований

Вы находитесь здесь

Викторины

Интерактивные викторины по вопросам с собеседований

Перейти к викторинам
Мы в Telegram
Новости проекта, общение с разработчиками, общение по интересам - присоединяйтесь!