- useBeforeUnload
- useClick
- useEventListener
- useFetch
- useHover
- useKeyPress
- useLocalStorage
- useDisableScroll
- useOnline
- useOnScreen
- usePortal
- usePrevious
- useRouter
- useStyle
- useTheme
- useTimer
- useWindowSize
- useCopyToClipboard
- useMutationObserver
Данный хук позволяет выполнять колбеки перед закрытием (перезагрузкой ) страницы
import { useEffect, useRef } from 'react'
export function useBeforeUnload(fn) {
const cb = useRef(fn)
useEffect(() => {
const onUnload = cb.current
window.addEventListener('beforeunload', onUnload)
return () => {
window.removeEventListener('beforeunload', onUnload)
}
}, [cb])
}
Более универсальная версия
import { useEffect } from 'react'
export const useBeforeUnload = (value) => {
const onUnload = (e) => {
let returnValue
if (typeof value === 'function') {
returnValue = value(e)
} else {
returnValue = value
}
if (returnValue) {
e.preventDefault()
e.returnValue = returnValue
}
return returnValue
}
useEffect(() => {
window.addEventListener('beforeunload', onUnload)
return () => window.removeEventListener('beforeunload', onUnload)
// eslint-disable-next-line
}, [])
}
Данные хуки позволяют запускать колбеки при клике внутри или снаружи целевого элемента
import { useEffect } from 'react'
export const useClickInside = (ref, cb) => {
const onClick = ({ target }) => {
if (ref.current && ref.current.contains(target)) {
cb()
}
}
useEffect(() => {
document.addEventListener('click', onClick)
return () => {
document.removeEventListener('click', onClick)
}
})
}
export const useClickOutside = (ref, cb) => {
const onClick = ({ target }) => {
if (ref.current && !ref.current.contains(target)) {
cb()
}
}
useEffect(() => {
document.addEventListener('click', onClick)
return () => {
document.removeEventListener('click', onClick)
}
})
}
// пример использования
// стили, чтобы было красиво
const containerStyles = {
height: '100vh',
display: 'flex',
justifyContent: 'space-evenly',
alignItems: 'center'
}
const wrapperStyles = {
display: 'inherit',
flexDirection: 'column',
alignItems: 'center'
}
const boxStyles = {
display: 'grid',
placeItems: 'center',
width: '100px',
height: '100px',
borderRadius: '4px',
boxShadow: '0 1px 2px rgba(0,0,0,.3)',
color: '#f0f0f0',
userSelect: 'none'
}
const textStyles = {
userSelect: 'none',
color: '#3c3c3c'
}
export function App() {
const insideRef = useRef(null)
const outsideRef = useRef(null)
const [insideCount, setInsideCount] = useState(0)
const [outsideCount, setOutsideCount] = useState(0)
const insideCb = () => {
setInsideCount((c) => c + 1)
}
const outsideCb = () => {
setOutsideCount((c) => c + 1)
}
useClickInside(insideRef, insideCb)
useClickOutside(outsideRef, outsideCb)
return (
<div style={containerStyles}>
<div style={wrapperStyles}>
<div
style={{ ...boxStyles, background: 'deepskyblue' }}
ref={insideRef}
>
Inside
</div>
<p style={textStyles}>Count: {insideCount}</p>
</div>
<div style={wrapperStyles}>
<div
style={{ ...boxStyles, background: 'mediumseagreen' }}
ref={outsideRef}
>
Outside
</div>
<p style={textStyles}>Count: {outsideCount}</p>
</div>
</div>
)
}
Данный хук позволяет регистрировать обработчик событий на целевом элементе
import { useRef, useEffect } from 'react'
export function useEventListener(ev, cb, $ = window) {
const cbRef = useRef()
// меняем значение ссылки на колбек при изменении колбека
useEffect(() => {
cbRef.current = cb
}, [cb])
useEffect(() => {
const listener = (ev) => cbRef.current(ev)
$.addEventListener(ev, listener)
return () => {
$.removeEventListener(ev, listener)
}
}, [ev, $])
}
// пример использования
export function App() {
const [coords, setCoords] = useState({ x: 0, y: 0 })
// небольшая оптимизация
const cb = useCallback(
({ clientX, clientY }) => {
setCoords({ x: clientX, y: clientY })
},
[setCoords]
)
useEventListener('mousemove', cb)
const { x, y } = coords
return (
<h1>
Mouse coords: {x}, {y}
</h1>
)
}
Хук для выполнения кэшируемых HTTP-запросов с помощью Fetch API
import { useState, useRef } from 'react'
export function useFetch(url, options) {
const [isLoading, setLoading] = useState(true)
const [response, setResponse] = useState(null)
const [error, setError] = useState(null)
const cache = useRef({})
useEffect(() => {
if (!url) return
async function fetchData() {
if (cache.current[url]) {
const data = cache.current[url]
setResponse(data)
} else {
try {
const response = await fetch(url, options)
const json = await response.json()
cache.current[url] = json
setResponse(json)
} catch (error) {
setError(error)
}
}
setLoading(false)
}
fetchData()
}, [url])
return { isLoading, response, error }
}
Хук для обработки наведения курсора на целевой элемент
import { useState, useEffect, useRef } from 'react'
export function useHover() {
const [value, setValue] = useState(false)
const ref = useRef(null)
const handleMouseOver = () => setValue(true)
const handleMouseOut = () => setValue(false)
useEffect(() => {
const node = ref.current
if (node) {
node.addEventListener('mouseover', handleMouseOver)
node.addEventListener('mouseout', handleMouseOut)
}
return () => {
node.removeEventListener('mouseover', handleMouseOver)
node.removeEventListener('mouseout', handleMouseOut)
}
}, [ref.current])
return [ref, value]
}
// пример использования
export function App() {
const [hoverRef, isHovered] = useHover()
return <div ref={hoverRef}>{isHovered ? '😊' : '☹️'}</div>
}
Хук для обработки нажатия клавиш клавиатуры
import { useState, useEffect } from 'react'
export function useKeyPress(target) {
const [keyPressed, setKeyPressed] = useState(false)
const onDown = ({ key }) => {
if (key === target) {
setKeyPressed(true)
}
}
const onUp = ({ key }) => {
if (key === target) {
setKeyPressed(false)
}
}
useEffect(() => {
window.addEventListener('keydown', onDown)
window.addEventListener('keyup', onUp)
return () => {
window.removeEventListener('keydown', onDown)
window.removeEventListener('keyup', onUp)
}
// eslint-disable-next-line
}, [])
return keyPressed
}
// пример использования
function App() {
const happy = useKeyPress('h')
const sad = useKeyPress('s')
return (
<>
<div>h, s</div>
<div>
{happy && '😊'}
{sad && '😢'}
</div>
</>
)
}
Хук для получения и записи значений в локальное хранилище
import { useState, useEffect } from 'react'
export function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
})
useEffect(() => {
const item = JSON.stringify(value)
window.localStorage.setItem(key, item)
// eslint-disable-next-line
}, [value])
return [value, setValue]
}
Хук для отключения прокрутки страницы, например, при вызове модального окна
import { useLayoutEffect } from 'react'
// другой кастомный хук, см. ниже
import { useStyle } from './useStyle'
export function useDisableScroll() {
const [, setOverflow] = useStyle('overflow')
useLayoutEffect(() => {
setOverflow('hidden')
return () => {
setOverflow('auto')
}
}, [])
}
Хук для определения статуса пользователя
import { useState, useEffect } from 'react'
const getStatus = () =>
typeof navigator !== 'undefined' && typeof navigator.onLine === 'boolean'
? navigator.onLine
: true
export const useOnline = () => {
const [status, setStatus] = useState(getStatus())
const setOnline = () => setStatus(true)
const setOffline = () => setStatus(false)
useEffect(() => {
window.addEventListener('online', setOnline)
window.addEventListener('offline', setOffline)
return () => {
window.removeEventListener('online', setOnline)
window.removeEventListener('offline', setOffline)
}
}, [])
return status
}
Хук для определения отображения элемента на экране
import { useEffect } from 'react'
export const useOnScreen = (ref, margin = '0px') => {
const [isIntersecting, setIntersecting] = useState(false)
useEffect(() => {
const O = new IntersectionObserver(
([entry]) => {
setIntersecting(entry.isIntersecting)
},
{ margin }
)
if (ref.current) {
O.observe(ref.current)
}
return () => {
O.unobserve(ref.current)
}
}, [])
return isIntersecting
}
Хук для создания порталов
import { useRef, useEffect } from 'react'
function createRoot(id) {
const root = document.createElement('div')
root.setAttribute('id', id)
return root
}
function addRoot(root) {
document.body.insertAdjacentElement('beforeend', root)
}
export function usePortal(id) {
const rootRef = useRef(null)
useEffect(
function setupElement() {
const existingParent = document.getElementById(id)
const parent = existingParent || createRoot(id)
if (!existingParent) {
addRoot(parent)
}
parent.appendChild(rootRef.current)
return function removeElement() {
rootRef.current.remove()
if (!parent.childElementCount) {
parent.remove()
}
}
},
[id]
)
function getRoot() {
if (!rootRef.current) {
rootRef.current = document.createElement('div')
}
return rootRef.current
}
return getRoot()
}
Хук для сохранения значения из предыдущего рендеринга
import { useEffect, useRef } from 'react'
export const usePrevious = (val) => {
const ref = useRef()
useEffect(() => {
ref.current = val
})
return ref.current
}
Хук, объединяющий в себе функционал всех хуков React Router DOM
import { useMemo } from 'react'
import {
useHistory,
useLocation,
useParams,
useRouteMatch
} from 'react-router-dom'
import queryString from 'query-string'
export const useRouter = () => {
const history = useHistory()
const location = useLocation()
const params = useParams()
const match = useRouteMatch()
return useMemo(
() => ({
push: history.push,
replace: history.replace,
pathname: location.pathname,
query: {
...queryString.parse(location.search),
...params
},
history,
location,
match
}),
[history, location, match, params]
)
}
Хук для получения и изменения стилей целевого элемента
import { useState, useEffect } from 'react'
export function useStyle(prop, $ = document.body) {
const [value, setValue] = useState(getComputedStyle($).getPropertyValue(prop))
useEffect(() => {
$.style.setProperty(prop, value)
}, [value])
return [value, setValue]
}
// пример использования
export function App() {
// другой кастомный хук, см. ниже
const { width, height } = useWindowSize()
const [color, setColor] = useStyle('color')
const [fontSize, setFontSize] = useStyle('font-size')
// имитация медиа запросов
useEffect(() => {
if (width > 1024) {
setColor('green')
setFontSize('2em')
} else if (width > 768) {
setColor('blue')
setFontSize('1.5em')
} else {
setColor('red')
setFontSize('1em')
}
}, [width])
return (
<>
<h1>
Window size: {width}, {height}
</h1>
<h2>Color: {color}</h2>
<h3>Font size: {fontSize}</h3>
</>
)
}
Хук для установки темы оформления страницы
import { useLayoutEffect } from 'react'
export function useTheme(theme) {
useLayoutEffect(() => {
for (const [prop, val] in theme) {
document.documentElement.style.setProperty(`--${prop}`, val)
}
}, [theme])
}
Хуки-обертки для setTimeout()
и setInterval()
import { useEffect, useRef } from 'react'
export function useTimeout(cb, ms) {
const cbRef = useRef()
useEffect(() => {
cbRef.current = cb
}, [cb])
useEffect(() => {
function tick() {
cbRef.current()
}
if (ms > 1) {
const id = setTimeout(tick, ms)
return () => {
clearTimeout(id)
}
}
}, [ms])
}
export function useInterval(cb, ms) {
const cbRef = useRef()
useEffect(() => {
cbRef.current = cb
}, [cb])
useEffect(() => {
function tick() {
cbRef.current()
}
if (ms > 1) {
const id = setInterval(tick, ms)
return () => {
clearInterval(id)
}
}
}, [ms])
}
Хук для получение размеров области просмотра
import { useState, useEffect } from 'react'
export function useWindowSize() {
const [size, setSize] = useState({})
useEffect(() => {
function onResize() {
setSize({
width: window.innerWidth,
height: window.innerHeight
})
}
window.addEventListener('resize', onResize)
onResize()
return () => window.removeEventListener('resize', onResize)
}, [])
return size
}
// пример использования
export function App() {
const { width, height } = useWindowSize()
// другой кастомный хук
const [color, setColor] = useStyle('color')
const [fontSize, setFontSize] = useStyle('font-size')
useEffect(() => {
if (width > 1024) {
setColor('green')
setFontSize('2em')
} else if (width > 768) {
setColor('blue')
setFontSize('1.5em')
} else {
setColor('red')
setFontSize('1em')
}
}, [width])
return (
<>
<h1>
Window size: {width}, {height}
</h1>
<h2>Color: {color}</h2>
<h3>Font size: {fontSize}</h3>
</>
)
}
Хук для копирования текста в буфер обмена
import { useState, useEffect } from 'react'
export const useCopyToClipboard = (resetTime) => {
const [copied, setCopied] = useState(false)
const copy = async (text) => {
await navigator.clipboard.writeText(text)
setCopied(true)
}
useEffect(() => {
let timerId
if (resetTime && copied) {
tmerId = setTimeout(() => {
setCopied(false)
}, resetTime)
}
return () => clearTimeout(timerId)
}, [])
return [copied, copy]
}
import { useState, useEffect } from 'react'
import { debounce } from 'lodash'
const DEFAULT_OPTIONS = {
config: { attributes: true, childList: true, subtree: true },
debounceTime: 0
}
export const useMutationObserver = (
target,
callback,
options = DEFAULT_OPTIONS
) => {
const [observer, setObserver] = useState(null)
useEffect(() => {
if (!callback || typeof callback !== 'function') {
return
}
const { debounceTime } = options
const observer = new MutationObserver(
debounceTime > 0 ? debounce(callback, debounceTime) : callback
)
setObserver(observer)
}, [callback, options, setObserver])
useEffect(() => {
if (!observer || !target) return
const { config } = options
try {
observer.observe(target, config)
} catch (e) {
console.error(e)
}
return () => {
if (observer) {
observer.disconnect()
}
}
}, [observer, target, options])
}