Кастомный хук — это обычная JavaScript-функция, имя которой начинается с use, внутри которой можно вызывать другие хуки. Это главный механизм переиспользования логики в React.
До хуков для переиспользования логики использовались HOC (Higher-Order Components) и render props — громоздкие паттерны. Кастомные хуки заменяют оба подхода элегантно и без лишней обёртки.
// Это кастомный хук — просто функция с use-префиксом
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
})
useEffect(() => {
const handler = () => setSize({
width: window.innerWidth,
height: window.innerHeight
})
window.addEventListener('resize', handler)
return () => window.removeEventListener('resize', handler)
}, [])
return size
}
// Используем в любом компоненте
function App() {
const { width, height } = useWindowSize()
return <p>Размер: {width} x {height}</p>
}Каждый вызов хука имеет изолированное состояние — два компонента, использующие useWindowSize, не делят состояние между собой.
Один из самых популярных кастомных хуков:
function useFetch(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let cancelled = false // защита от race condition
setLoading(true)
fetch(url)
.then(r => r.json())
.then(data => {
if (!cancelled) {
setData(data)
setLoading(false)
}
})
.catch(err => {
if (!cancelled) {
setError(err.message)
setLoading(false)
}
})
return () => { cancelled = true } // cleanup при смене url
}, [url])
return { data, loading, error }
}
// Использование — чисто и просто
function UserProfile({ id }) {
const { data, loading, error } = useFetch(`/api/users/${id}`)
if (loading) return <Spinner />
if (error) return <Error message={error} />
return <Profile user={data} />
}function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const stored = localStorage.getItem(key)
return stored ? JSON.parse(stored) : initialValue
} catch {
return initialValue
}
})
const setStoredValue = (newValue) => {
try {
const valueToStore = typeof newValue === 'function'
? newValue(value)
: newValue
setValue(valueToStore)
localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (err) {
console.error('localStorage error:', err)
}
}
return [value, setStoredValue]
}
// Использование как обычный useState, но с персистентностью
const [theme, setTheme] = useLocalStorage('theme', 'light')Полезен для поиска — чтобы не делать запрос при каждом нажатии клавиши:
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => clearTimeout(timer) // сбрасываем таймер при каждом изменении
}, [value, delay])
return debouncedValue
}
function SearchInput() {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 500)
// Этот эффект запустится только через 500мс после остановки ввода
useEffect(() => {
if (debouncedQuery) fetchResults(debouncedQuery)
}, [debouncedQuery])
return <input value={query} onChange={e => setQuery(e.target.value)} />
}Кастомные хуки подчиняются тем же правилам, что и встроенные:
1. Вызывайте хуки только на верхнем уровне — не внутри условий, циклов, вложенных функций
2. Вызывайте хуки только в React-компонентах или других кастомных хуках
3. Имя должно начинаться с use
function useOnClickOutside(ref, handler) {
useEffect(() => {
const listener = (event) => {
if (!ref.current || ref.current.contains(event.target)) return
handler(event)
}
document.addEventListener('mousedown', listener)
return () => document.removeEventListener('mousedown', listener)
}, [ref, handler])
}
// Использование для закрытия дропдауна
function Dropdown() {
const [open, setOpen] = useState(false)
const ref = useRef(null)
useOnClickOutside(ref, () => setOpen(false))
return <div ref={ref}>{open && <Menu />}</div>
}Реализация кастомных хуков в виде функций с замыканием: useFetch, useDebounce, useLocalStorage без React
// Кастомные хуки — это просто функции, инкапсулирующие логику.
// Реализуем их на чистом JS с тем же интерфейсом, что в React.
// --- useFetch: загрузка данных ---
function useFetch(url) {
// Внутреннее состояние хука
const state = {
data: null,
loading: true,
error: null,
subscribers: []
}
const notify = () => state.subscribers.forEach(fn => fn({ ...state }))
// Логика загрузки
let cancelled = false
fetch(url)
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`)
return r.json()
})
.then(data => {
if (cancelled) return
state.data = data
state.loading = false
notify()
})
.catch(err => {
if (cancelled) return
state.error = err.message
state.loading = false
notify()
})
return {
getState: () => ({ ...state }),
subscribe: (fn) => { state.subscribers.push(fn) },
cancel: () => { cancelled = true }
}
}
// --- useDebounce: задержка значения ---
function useDebounce(initialValue, delay) {
let currentValue = initialValue
let debouncedValue = initialValue
let timerId = null
const subscribers = []
const notify = () => subscribers.forEach(fn => fn(debouncedValue))
return {
setValue(newValue) {
currentValue = newValue
// Сбрасываем предыдущий таймер — как useEffect cleanup
clearTimeout(timerId)
timerId = setTimeout(() => {
debouncedValue = currentValue
console.log(` [useDebounce] обновлено через ${delay}мс: "${debouncedValue}"`)
notify()
}, delay)
},
getValue: () => debouncedValue,
subscribe: (fn) => subscribers.push(fn),
destroy: () => clearTimeout(timerId)
}
}
// --- useLocalStorage: синхронизация с хранилищем ---
function useLocalStorage(key, initialValue) {
// Имитируем localStorage (в реальности используем window.localStorage)
const storage = new Map()
const read = () => {
try {
const item = storage.get(key)
return item !== undefined ? JSON.parse(item) : initialValue
} catch {
return initialValue
}
}
let value = read()
return {
getValue: () => value,
setValue(newValue) {
value = typeof newValue === 'function' ? newValue(value) : newValue
storage.set(key, JSON.stringify(value))
console.log(' [useLocalStorage] "' + key + '" сохранено:', value)
},
// Симуляция чтения при "монтировании" нового компонента
init: () => { value = read(); return value }
}
}
// --- Демонстрация ---
console.log('=== useDebounce ===')
const search = useDebounce('', 300)
search.subscribe((val) => console.log(' [поиск] запрос к API: "' + val + '"'))
// Быстрый ввод — только последнее значение дойдёт до API
console.log('Пользователь быстро набирает: r -> re -> rea -> реакт')
search.setValue('r')
search.setValue('re')
search.setValue('rea')
search.setValue('реакт')
// Только 'реакт' попадёт в API через 300мс
setTimeout(() => {
console.log('Дебаунсированное значение:', search.getValue())
search.destroy()
console.log('
=== useLocalStorage ===')
const themeStorage = useLocalStorage('theme', 'light')
console.log('Начальное значение:', themeStorage.getValue()) // 'light'
themeStorage.setValue('dark')
console.log('После смены:', themeStorage.getValue()) // 'dark'
themeStorage.setValue(prev => prev === 'dark' ? 'light' : 'dark')
console.log('После toggle:', themeStorage.getValue()) // 'light'
console.log('
=== useFetch (симуляция) ===')
// В реальном коде: const { data, loading, error } = useFetch('/api/users')
console.log('В React: const { data, loading, error } = useFetch(url)')
console.log('Хук инкапсулирует: fetch, useState, useEffect, отмену запроса')
}, 500)Кастомный хук — это обычная JavaScript-функция, имя которой начинается с use, внутри которой можно вызывать другие хуки. Это главный механизм переиспользования логики в React.
До хуков для переиспользования логики использовались HOC (Higher-Order Components) и render props — громоздкие паттерны. Кастомные хуки заменяют оба подхода элегантно и без лишней обёртки.
// Это кастомный хук — просто функция с use-префиксом
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
})
useEffect(() => {
const handler = () => setSize({
width: window.innerWidth,
height: window.innerHeight
})
window.addEventListener('resize', handler)
return () => window.removeEventListener('resize', handler)
}, [])
return size
}
// Используем в любом компоненте
function App() {
const { width, height } = useWindowSize()
return <p>Размер: {width} x {height}</p>
}Каждый вызов хука имеет изолированное состояние — два компонента, использующие useWindowSize, не делят состояние между собой.
Один из самых популярных кастомных хуков:
function useFetch(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let cancelled = false // защита от race condition
setLoading(true)
fetch(url)
.then(r => r.json())
.then(data => {
if (!cancelled) {
setData(data)
setLoading(false)
}
})
.catch(err => {
if (!cancelled) {
setError(err.message)
setLoading(false)
}
})
return () => { cancelled = true } // cleanup при смене url
}, [url])
return { data, loading, error }
}
// Использование — чисто и просто
function UserProfile({ id }) {
const { data, loading, error } = useFetch(`/api/users/${id}`)
if (loading) return <Spinner />
if (error) return <Error message={error} />
return <Profile user={data} />
}function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const stored = localStorage.getItem(key)
return stored ? JSON.parse(stored) : initialValue
} catch {
return initialValue
}
})
const setStoredValue = (newValue) => {
try {
const valueToStore = typeof newValue === 'function'
? newValue(value)
: newValue
setValue(valueToStore)
localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (err) {
console.error('localStorage error:', err)
}
}
return [value, setStoredValue]
}
// Использование как обычный useState, но с персистентностью
const [theme, setTheme] = useLocalStorage('theme', 'light')Полезен для поиска — чтобы не делать запрос при каждом нажатии клавиши:
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => clearTimeout(timer) // сбрасываем таймер при каждом изменении
}, [value, delay])
return debouncedValue
}
function SearchInput() {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 500)
// Этот эффект запустится только через 500мс после остановки ввода
useEffect(() => {
if (debouncedQuery) fetchResults(debouncedQuery)
}, [debouncedQuery])
return <input value={query} onChange={e => setQuery(e.target.value)} />
}Кастомные хуки подчиняются тем же правилам, что и встроенные:
1. Вызывайте хуки только на верхнем уровне — не внутри условий, циклов, вложенных функций
2. Вызывайте хуки только в React-компонентах или других кастомных хуках
3. Имя должно начинаться с use
function useOnClickOutside(ref, handler) {
useEffect(() => {
const listener = (event) => {
if (!ref.current || ref.current.contains(event.target)) return
handler(event)
}
document.addEventListener('mousedown', listener)
return () => document.removeEventListener('mousedown', listener)
}, [ref, handler])
}
// Использование для закрытия дропдауна
function Dropdown() {
const [open, setOpen] = useState(false)
const ref = useRef(null)
useOnClickOutside(ref, () => setOpen(false))
return <div ref={ref}>{open && <Menu />}</div>
}Реализация кастомных хуков в виде функций с замыканием: useFetch, useDebounce, useLocalStorage без React
// Кастомные хуки — это просто функции, инкапсулирующие логику.
// Реализуем их на чистом JS с тем же интерфейсом, что в React.
// --- useFetch: загрузка данных ---
function useFetch(url) {
// Внутреннее состояние хука
const state = {
data: null,
loading: true,
error: null,
subscribers: []
}
const notify = () => state.subscribers.forEach(fn => fn({ ...state }))
// Логика загрузки
let cancelled = false
fetch(url)
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`)
return r.json()
})
.then(data => {
if (cancelled) return
state.data = data
state.loading = false
notify()
})
.catch(err => {
if (cancelled) return
state.error = err.message
state.loading = false
notify()
})
return {
getState: () => ({ ...state }),
subscribe: (fn) => { state.subscribers.push(fn) },
cancel: () => { cancelled = true }
}
}
// --- useDebounce: задержка значения ---
function useDebounce(initialValue, delay) {
let currentValue = initialValue
let debouncedValue = initialValue
let timerId = null
const subscribers = []
const notify = () => subscribers.forEach(fn => fn(debouncedValue))
return {
setValue(newValue) {
currentValue = newValue
// Сбрасываем предыдущий таймер — как useEffect cleanup
clearTimeout(timerId)
timerId = setTimeout(() => {
debouncedValue = currentValue
console.log(` [useDebounce] обновлено через ${delay}мс: "${debouncedValue}"`)
notify()
}, delay)
},
getValue: () => debouncedValue,
subscribe: (fn) => subscribers.push(fn),
destroy: () => clearTimeout(timerId)
}
}
// --- useLocalStorage: синхронизация с хранилищем ---
function useLocalStorage(key, initialValue) {
// Имитируем localStorage (в реальности используем window.localStorage)
const storage = new Map()
const read = () => {
try {
const item = storage.get(key)
return item !== undefined ? JSON.parse(item) : initialValue
} catch {
return initialValue
}
}
let value = read()
return {
getValue: () => value,
setValue(newValue) {
value = typeof newValue === 'function' ? newValue(value) : newValue
storage.set(key, JSON.stringify(value))
console.log(' [useLocalStorage] "' + key + '" сохранено:', value)
},
// Симуляция чтения при "монтировании" нового компонента
init: () => { value = read(); return value }
}
}
// --- Демонстрация ---
console.log('=== useDebounce ===')
const search = useDebounce('', 300)
search.subscribe((val) => console.log(' [поиск] запрос к API: "' + val + '"'))
// Быстрый ввод — только последнее значение дойдёт до API
console.log('Пользователь быстро набирает: r -> re -> rea -> реакт')
search.setValue('r')
search.setValue('re')
search.setValue('rea')
search.setValue('реакт')
// Только 'реакт' попадёт в API через 300мс
setTimeout(() => {
console.log('Дебаунсированное значение:', search.getValue())
search.destroy()
console.log('
=== useLocalStorage ===')
const themeStorage = useLocalStorage('theme', 'light')
console.log('Начальное значение:', themeStorage.getValue()) // 'light'
themeStorage.setValue('dark')
console.log('После смены:', themeStorage.getValue()) // 'dark'
themeStorage.setValue(prev => prev === 'dark' ? 'light' : 'dark')
console.log('После toggle:', themeStorage.getValue()) // 'light'
console.log('
=== useFetch (симуляция) ===')
// В реальном коде: const { data, loading, error } = useFetch('/api/users')
console.log('В React: const { data, loading, error } = useFetch(url)')
console.log('Хук инкапсулирует: fetch, useState, useEffect, отмену запроса')
}, 500)Напиши кастомный хук useCounter(initial) который возвращает { count, increment, decrement, reset }. Хук использует useState внутри. Затем напиши кастомный хук useToggle(initial) возвращающий [value, toggle]. Используй оба хука в компоненте App.
useCounter использует useState(initial). increment: c => c + 1. reset: () => setCount(initial). useToggle: setValue(v => !v). В App вызывай хуки как обычные функции: useCounter(0), useToggle(true).