TypeScript часто выводит тип из начального значения. Но когда начальное значение null или неоднозначно — явный параметр необходим:
// Тип выводится автоматически:
const [count, setCount] = useState(0) // number
const [name, setName] = useState('') // string
const [active, setActive] = useState(false) // boolean
// Явный тип нужен:
const [user, setUser] = useState<User | null>(null)
const [items, setItems] = useState<string[]>([])
const [status, setStatus] = useState<'idle' | 'loading' | 'error'>('idle')useRef имеет две роли — хранение DOM-ссылки и хранение изменяемого значения:
// DOM-ссылка: передаём null, тип = HTMLInputElement | null
const inputRef = useRef<HTMLInputElement>(null)
// Использование:
useEffect(() => {
inputRef.current?.focus() // current может быть null до монтирования
}, [])
// Изменяемое значение (не DOM): передаём начальное значение
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const countRef = useRef(0) // изменяем без ре-рендера// Тип возвращаемой функции выводится автоматически:
const handleClick = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.currentTarget.id)
}, [])
// Явный тип если нужно передать в пропс:
const handleChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>(
(e) => setValue(e.target.value),
[]
)// Тип выводится из возвращаемого значения:
const filtered = useMemo(
() => items.filter((item) => item.active),
[items]
) // тип: Item[]
// Явный параметр если нужно:
const result = useMemo<Record<string, number>>(
() => computeExpensive(data),
[data]
)// Проблема: TypeScript выводит тип как (string | boolean)[]
function useToggle(initial: boolean) {
const [on, setOn] = useState(initial)
const toggle = useCallback(() => setOn(v => !v), [])
return [on, toggle] // ошибка! выведет (boolean | (() => void))[]
}
// Решение 1: as const
function useToggle(initial: boolean) {
const [on, setOn] = useState(initial)
const toggle = useCallback(() => setOn(v => !v), [])
return [on, toggle] as const // readonly [boolean, () => void]
}
// Решение 2: явный тип возврата
function useToggle(initial: boolean): [boolean, () => void] {
const [on, setOn] = useState(initial)
const toggle = useCallback(() => setOn(v => !v), [])
return [on, toggle]
}function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
fetch(url)
.then(r => r.json() as Promise<T>)
.then(setData)
.catch(setError)
.finally(() => setLoading(false))
}, [url])
return { data, loading, error }
}
// Использование:
interface User { id: number; name: string }
const { data, loading } = useFetch<User[]>('/api/users')
// data: User[] | nulltype State =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: User[] }
| { status: 'error'; message: string }
type Action =
| { type: 'FETCH_START' }
| { type: 'FETCH_SUCCESS'; payload: User[] }
| { type: 'FETCH_ERROR'; message: string }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'FETCH_START': return { status: 'loading' }
case 'FETCH_SUCCESS': return { status: 'success', data: action.payload }
case 'FETCH_ERROR': return { status: 'error', message: action.message }
}
}Реализация кастомных React-хуков в чистом JS: useFetch, useLocalStorage, useDebounce с полной логикой
// Реализуем популярные кастомные хуки без React — только логика.
// В TypeScript каждый хук имеет строгие дженерик-типы.
// --- Упрощённая система хуков ---
const hookState = new Map()
let currentHookKey = null
let hookCursor = 0
function useState(initialValue) {
const key = currentHookKey
const idx = hookCursor++
const stateKey = key + ':' + idx
if (!hookState.has(stateKey)) {
hookState.set(stateKey, initialValue)
}
const value = hookState.get(stateKey)
const setter = (newValue) => {
const next = typeof newValue === 'function' ? newValue(hookState.get(stateKey)) : newValue
hookState.set(stateKey, next)
}
return [value, setter]
}
function runHook(name, hookFn, ...args) {
currentHookKey = name
hookCursor = 0
return hookFn(...args)
}
// --- useLocalStorage<T> ---
// TS: function useLocalStorage<T>(key: string, initialValue: T): [T, (v: T) => void]
function useLocalStorage(key, initialValue) {
const storage = useLocalStorage._store || (useLocalStorage._store = {})
const [stored, setStored] = useState(() => {
try {
return storage[key] !== undefined ? storage[key] : initialValue
} catch {
return initialValue
}
})
const setValue = (value) => {
const newValue = typeof value === 'function' ? value(stored) : value
storage[key] = newValue
setStored(newValue)
}
return [stored, setValue]
}
// --- useDebounce<T> ---
// TS: function useDebounce<T>(value: T, delay: number): T
function useDebounce(value, delay) {
// В реальном React здесь useEffect + setTimeout
// Симулируем: просто возвращаем значение (в тестах delay=0)
const [debouncedValue, setDebouncedValue] = useState(value)
// Сразу обновляем для симуляции
setDebouncedValue(value)
return debouncedValue
}
// --- useFetch<T> ---
// TS: function useFetch<T>(url: string): { data: T | null, loading: boolean, error: Error | null }
function useFetch(url) {
const [state, setState] = useState({
data: null,
loading: false,
error: null
})
// Возвращаем функцию fetch для вызова вручную (в реальном хуке — useEffect)
const fetch = async (mockData) => {
setState({ data: null, loading: true, error: null })
try {
// Симуляция async fetch
await new Promise(resolve => setTimeout(resolve, 10))
setState({ data: mockData, loading: false, error: null })
} catch (err) {
setState({ data: null, loading: false, error: err })
}
}
return { ...state, fetch }
}
// --- useToggle ---
// TS: function useToggle(initial: boolean): [boolean, () => void]
function useToggle(initial) {
const [on, setOn] = useState(initial)
const toggle = () => setOn(v => !v)
// В TS: return [on, toggle] as const
return [on, toggle]
}
// --- usePrevious<T> ---
// TS: function usePrevious<T>(value: T): T | undefined
function usePrevious(value) {
const prev = usePrevious._store || (usePrevious._store = new Map())
const key = 'prev:' + currentHookKey
const previous = prev.get(key)
prev.set(key, value)
return previous
}
// --- Демонстрация ---
console.log('=== useLocalStorage ===')
const [theme, setTheme] = runHook('storage1', useLocalStorage, 'theme', 'light')
console.log('initial theme:', theme) // light
setTheme('dark')
const [theme2] = runHook('storage1', useLocalStorage, 'theme', 'light')
console.log('after setTheme("dark"):', theme2) // dark (сохранено в storage)
console.log('\n=== useToggle ===')
const [isOpen, toggle] = runHook('toggle1', useToggle, false)
console.log('initial:', isOpen) // false
toggle()
const [isOpen2] = runHook('toggle1', useToggle, false)
console.log('after toggle():', hookState.get('toggle1:0')) // true
console.log('\n=== usePrevious ===')
const prev1 = runHook('prev1', usePrevious, 'first')
console.log('prev (first call):', prev1) // undefined
const prev2 = runHook('prev1', usePrevious, 'second')
console.log('prev (second call):', prev2) // first
const prev3 = runHook('prev1', usePrevious, 'third')
console.log('prev (third call):', prev3) // second
console.log('\n=== useFetch ===')
const { data, loading, error, fetch } = runHook('fetch1', useFetch, '/api/users')
console.log('initial state: data=' + data + ', loading=' + loading)
// Async fetch
fetch([{ id: 1, name: 'Алексей' }, { id: 2, name: 'Мария' }]).then(() => {
const state = hookState.get('fetch1:0')
console.log('after fetch: loading=' + state.loading)
console.log('data:', state.data)
})TypeScript часто выводит тип из начального значения. Но когда начальное значение null или неоднозначно — явный параметр необходим:
// Тип выводится автоматически:
const [count, setCount] = useState(0) // number
const [name, setName] = useState('') // string
const [active, setActive] = useState(false) // boolean
// Явный тип нужен:
const [user, setUser] = useState<User | null>(null)
const [items, setItems] = useState<string[]>([])
const [status, setStatus] = useState<'idle' | 'loading' | 'error'>('idle')useRef имеет две роли — хранение DOM-ссылки и хранение изменяемого значения:
// DOM-ссылка: передаём null, тип = HTMLInputElement | null
const inputRef = useRef<HTMLInputElement>(null)
// Использование:
useEffect(() => {
inputRef.current?.focus() // current может быть null до монтирования
}, [])
// Изменяемое значение (не DOM): передаём начальное значение
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const countRef = useRef(0) // изменяем без ре-рендера// Тип возвращаемой функции выводится автоматически:
const handleClick = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.currentTarget.id)
}, [])
// Явный тип если нужно передать в пропс:
const handleChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>(
(e) => setValue(e.target.value),
[]
)// Тип выводится из возвращаемого значения:
const filtered = useMemo(
() => items.filter((item) => item.active),
[items]
) // тип: Item[]
// Явный параметр если нужно:
const result = useMemo<Record<string, number>>(
() => computeExpensive(data),
[data]
)// Проблема: TypeScript выводит тип как (string | boolean)[]
function useToggle(initial: boolean) {
const [on, setOn] = useState(initial)
const toggle = useCallback(() => setOn(v => !v), [])
return [on, toggle] // ошибка! выведет (boolean | (() => void))[]
}
// Решение 1: as const
function useToggle(initial: boolean) {
const [on, setOn] = useState(initial)
const toggle = useCallback(() => setOn(v => !v), [])
return [on, toggle] as const // readonly [boolean, () => void]
}
// Решение 2: явный тип возврата
function useToggle(initial: boolean): [boolean, () => void] {
const [on, setOn] = useState(initial)
const toggle = useCallback(() => setOn(v => !v), [])
return [on, toggle]
}function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
fetch(url)
.then(r => r.json() as Promise<T>)
.then(setData)
.catch(setError)
.finally(() => setLoading(false))
}, [url])
return { data, loading, error }
}
// Использование:
interface User { id: number; name: string }
const { data, loading } = useFetch<User[]>('/api/users')
// data: User[] | nulltype State =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: User[] }
| { status: 'error'; message: string }
type Action =
| { type: 'FETCH_START' }
| { type: 'FETCH_SUCCESS'; payload: User[] }
| { type: 'FETCH_ERROR'; message: string }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'FETCH_START': return { status: 'loading' }
case 'FETCH_SUCCESS': return { status: 'success', data: action.payload }
case 'FETCH_ERROR': return { status: 'error', message: action.message }
}
}Реализация кастомных React-хуков в чистом JS: useFetch, useLocalStorage, useDebounce с полной логикой
// Реализуем популярные кастомные хуки без React — только логика.
// В TypeScript каждый хук имеет строгие дженерик-типы.
// --- Упрощённая система хуков ---
const hookState = new Map()
let currentHookKey = null
let hookCursor = 0
function useState(initialValue) {
const key = currentHookKey
const idx = hookCursor++
const stateKey = key + ':' + idx
if (!hookState.has(stateKey)) {
hookState.set(stateKey, initialValue)
}
const value = hookState.get(stateKey)
const setter = (newValue) => {
const next = typeof newValue === 'function' ? newValue(hookState.get(stateKey)) : newValue
hookState.set(stateKey, next)
}
return [value, setter]
}
function runHook(name, hookFn, ...args) {
currentHookKey = name
hookCursor = 0
return hookFn(...args)
}
// --- useLocalStorage<T> ---
// TS: function useLocalStorage<T>(key: string, initialValue: T): [T, (v: T) => void]
function useLocalStorage(key, initialValue) {
const storage = useLocalStorage._store || (useLocalStorage._store = {})
const [stored, setStored] = useState(() => {
try {
return storage[key] !== undefined ? storage[key] : initialValue
} catch {
return initialValue
}
})
const setValue = (value) => {
const newValue = typeof value === 'function' ? value(stored) : value
storage[key] = newValue
setStored(newValue)
}
return [stored, setValue]
}
// --- useDebounce<T> ---
// TS: function useDebounce<T>(value: T, delay: number): T
function useDebounce(value, delay) {
// В реальном React здесь useEffect + setTimeout
// Симулируем: просто возвращаем значение (в тестах delay=0)
const [debouncedValue, setDebouncedValue] = useState(value)
// Сразу обновляем для симуляции
setDebouncedValue(value)
return debouncedValue
}
// --- useFetch<T> ---
// TS: function useFetch<T>(url: string): { data: T | null, loading: boolean, error: Error | null }
function useFetch(url) {
const [state, setState] = useState({
data: null,
loading: false,
error: null
})
// Возвращаем функцию fetch для вызова вручную (в реальном хуке — useEffect)
const fetch = async (mockData) => {
setState({ data: null, loading: true, error: null })
try {
// Симуляция async fetch
await new Promise(resolve => setTimeout(resolve, 10))
setState({ data: mockData, loading: false, error: null })
} catch (err) {
setState({ data: null, loading: false, error: err })
}
}
return { ...state, fetch }
}
// --- useToggle ---
// TS: function useToggle(initial: boolean): [boolean, () => void]
function useToggle(initial) {
const [on, setOn] = useState(initial)
const toggle = () => setOn(v => !v)
// В TS: return [on, toggle] as const
return [on, toggle]
}
// --- usePrevious<T> ---
// TS: function usePrevious<T>(value: T): T | undefined
function usePrevious(value) {
const prev = usePrevious._store || (usePrevious._store = new Map())
const key = 'prev:' + currentHookKey
const previous = prev.get(key)
prev.set(key, value)
return previous
}
// --- Демонстрация ---
console.log('=== useLocalStorage ===')
const [theme, setTheme] = runHook('storage1', useLocalStorage, 'theme', 'light')
console.log('initial theme:', theme) // light
setTheme('dark')
const [theme2] = runHook('storage1', useLocalStorage, 'theme', 'light')
console.log('after setTheme("dark"):', theme2) // dark (сохранено в storage)
console.log('\n=== useToggle ===')
const [isOpen, toggle] = runHook('toggle1', useToggle, false)
console.log('initial:', isOpen) // false
toggle()
const [isOpen2] = runHook('toggle1', useToggle, false)
console.log('after toggle():', hookState.get('toggle1:0')) // true
console.log('\n=== usePrevious ===')
const prev1 = runHook('prev1', usePrevious, 'first')
console.log('prev (first call):', prev1) // undefined
const prev2 = runHook('prev1', usePrevious, 'second')
console.log('prev (second call):', prev2) // first
const prev3 = runHook('prev1', usePrevious, 'third')
console.log('prev (third call):', prev3) // second
console.log('\n=== useFetch ===')
const { data, loading, error, fetch } = runHook('fetch1', useFetch, '/api/users')
console.log('initial state: data=' + data + ', loading=' + loading)
// Async fetch
fetch([{ id: 1, name: 'Алексей' }, { id: 2, name: 'Мария' }]).then(() => {
const state = hookState.get('fetch1:0')
console.log('after fetch: loading=' + state.loading)
console.log('data:', state.data)
})Реализуй кастомный хук `useForm(initialValues, validate)`. Параметры: `initialValues` — объект с начальными значениями полей, `validate` — функция валидации, получает значения и возвращает объект ошибок `{ fieldName: errorMessage }`. Хук возвращает: `values` (текущие значения), `errors` (ошибки), `handleChange(field, value)` (обновляет поле), `handleSubmit(onSuccess)` (запускает валидацию, при успехе вызывает `onSuccess(values)`).
handleChange: setValues(prev => ({ ...prev, [field]: value })). handleSubmit: const errs = validate(_state["form:values"]); setErrors(errs); if (Object.keys(errs).length === 0) onSuccess(_state["form:values"]). Обнули ошибки при успешном сабмите через setErrors({}).
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке