useRef, useContext, useReducer
useRef возвращает мутабельный объект { current: T }, который сохраняется между рендерами. Изменение ref.current не вызывает рендер.
DOM-ссылки
Заголовок раздела «DOM-ссылки»import { useRef, useEffect } from 'react';
function AutoFocusInput() { const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => { inputRef.current?.focus(); // безопасный доступ через ?. }, []);
return <input ref={inputRef} placeholder="Автофокус" />;}
// Управление медиаfunction VideoPlayer({ src }: { src: string }) { const videoRef = useRef<HTMLVideoElement>(null);
function play() { videoRef.current?.play(); } function pause() { videoRef.current?.pause(); }
return ( <> <video ref={videoRef} src={src} /> <button onClick={play}>▶</button> <button onClick={pause}>⏸</button> </> );}Мутабельные значения (без рендера)
Заголовок раздела «Мутабельные значения (без рендера)»// Хранить предыдущее значениеfunction usePrevious<T>(value: T): T | undefined { const ref = useRef<T>(); useEffect(() => { ref.current = value; }); // обновляется после каждого рендера return ref.current; // предыдущее значение}
// Хранить ID интервала/таймераfunction Countdown({ seconds }: { seconds: number }) { const [remaining, setRemaining] = useState(seconds); const intervalRef = useRef<ReturnType<typeof setInterval>>();
function start() { intervalRef.current = setInterval(() => { setRemaining(prev => { if (prev <= 1) { clearInterval(intervalRef.current); return 0; } return prev - 1; }); }, 1000); }
function stop() { clearInterval(intervalRef.current); }
return ( <> <div>{remaining}с</div> <button onClick={start}>Старт</button> <button onClick={stop}>Стоп</button> </> );}useImperativeHandle
Заголовок раздела «useImperativeHandle»Ограничивает что родитель видит через ref:
interface DialogHandle { open: () => void; close: () => void;}
const Dialog = React.forwardRef<DialogHandle, React.PropsWithChildren>( ({ children }, ref) => { const [isOpen, setIsOpen] = useState(false);
useImperativeHandle(ref, () => ({ open: () => setIsOpen(true), close: () => setIsOpen(false), }));
if (!isOpen) return null; return <div className="dialog">{children}</div>; });
function App() { const dialogRef = useRef<DialogHandle>(null);
return ( <> <Dialog ref={dialogRef}>Содержимое</Dialog> <button onClick={() => dialogRef.current?.open()}>Открыть</button> </> );}useContext
Заголовок раздела «useContext»Создание типизированного контекста
Заголовок раздела «Создание типизированного контекста»import { createContext, useContext, useState } from 'react';
interface AuthContextValue { user: User | null; login: (credentials: Credentials) => Promise<void>; logout: () => void;}
// null как дефолт — позволяет обнаружить использование вне провайдераconst AuthContext = createContext<AuthContextValue | null>(null);
export function AuthProvider({ children }: React.PropsWithChildren) { const [user, setUser] = useState<User | null>(null);
const login = async (credentials: Credentials) => { const user = await authService.login(credentials); setUser(user); };
const logout = () => { authService.logout(); setUser(null); };
return ( <AuthContext.Provider value={{ user, login, logout }}> {children} </AuthContext.Provider> );}
// Кастомный хук с проверкой контекстаexport function useAuth(): AuthContextValue { const ctx = useContext(AuthContext); if (!ctx) { throw new Error('useAuth must be used within AuthProvider'); } return ctx;}Оптимизация производительности контекста
Заголовок раздела «Оптимизация производительности контекста»// Проблема: все потребители перерисовываются при любом изменении контекста
// ✅ Решение: разделить контекст на частиconst UserContext = createContext<User | null>(null);const AuthActionsContext = createContext<AuthActions | null>(null);
function AuthProvider({ children }: React.PropsWithChildren) { const [user, setUser] = useState<User | null>(null);
// Actions не меняются — мемоизируем const actions = useMemo(() => ({ login: async (creds: Credentials) => { /* ... */ }, logout: () => setUser(null), }), []);
return ( <AuthActionsContext.Provider value={actions}> <UserContext.Provider value={user}> {children} </UserContext.Provider> </AuthActionsContext.Provider> );}
// Компоненты подписываются только на нужный контекстfunction UserAvatar() { const user = useContext(UserContext); // перерисовывается при изменении user return <img src={user?.avatar} />;}
function LogoutButton() { const actions = useContext(AuthActionsContext); // НЕ перерисовывается при изменении user return <button onClick={actions?.logout}>Выйти</button>;}useReducer
Заголовок раздела «useReducer»useReducer — полноценная замена useState для сложного состояния с несколькими sub-значениями или когда следующее состояние зависит от предыдущего.
Базовый паттерн
Заголовок раздела «Базовый паттерн»import { useReducer } from 'react';
// 1. Определяем тип состоянияinterface CounterState { count: number; step: number;}
// 2. Определяем действия (discriminated union)type CounterAction = | { type: 'INCREMENT' } | { type: 'DECREMENT' } | { type: 'SET_STEP'; payload: number } | { type: 'RESET' };
// 3. Редюсер — чистая функцияfunction counterReducer(state: CounterState, action: CounterAction): CounterState { switch (action.type) { case 'INCREMENT': return { ...state, count: state.count + state.step }; case 'DECREMENT': return { ...state, count: state.count - state.step }; case 'SET_STEP': return { ...state, step: action.payload }; case 'RESET': return { count: 0, step: 1 }; default: return state; // обязательно для TypeScript exhaustiveness check }}
function Counter() { const [state, dispatch] = useReducer(counterReducer, { count: 0, step: 1 });
return ( <div> <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button> <span>{state.count}</span> <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button> <input type="number" value={state.step} onChange={e => dispatch({ type: 'SET_STEP', payload: Number(e.target.value) })} /> <button onClick={() => dispatch({ type: 'RESET' })}>Сброс</button> </div> );}useReducer для форм
Заголовок раздела «useReducer для форм»interface FormState { values: { name: string; email: string; message: string }; errors: Partial<Record<'name' | 'email' | 'message', string>>; isSubmitting: boolean;}
type FormAction = | { type: 'FIELD_CHANGE'; field: keyof FormState['values']; value: string } | { type: 'VALIDATE'; errors: FormState['errors'] } | { type: 'SUBMIT_START' } | { type: 'SUBMIT_SUCCESS' } | { type: 'SUBMIT_ERROR'; error: string };
const initialState: FormState = { values: { name: '', email: '', message: '' }, errors: {}, isSubmitting: false,};
function formReducer(state: FormState, action: FormAction): FormState { switch (action.type) { case 'FIELD_CHANGE': return { ...state, values: { ...state.values, [action.field]: action.value }, errors: { ...state.errors, [action.field]: undefined }, }; case 'VALIDATE': return { ...state, errors: action.errors }; case 'SUBMIT_START': return { ...state, isSubmitting: true }; case 'SUBMIT_SUCCESS': return { ...initialState }; // сброс формы case 'SUBMIT_ERROR': return { ...state, isSubmitting: false, errors: { message: action.error } }; default: return state; }}
function ContactForm() { const [state, dispatch] = useReducer(formReducer, initialState);
function handleChange(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) { dispatch({ type: 'FIELD_CHANGE', field: e.target.name as keyof FormState['values'], value: e.target.value, }); }
async function handleSubmit(e: React.FormEvent) { e.preventDefault(); dispatch({ type: 'SUBMIT_START' }); try { await sendMessage(state.values); dispatch({ type: 'SUBMIT_SUCCESS' }); } catch (err) { dispatch({ type: 'SUBMIT_ERROR', error: String(err) }); } }
return ( <form onSubmit={handleSubmit}> <input name="name" value={state.values.name} onChange={handleChange} /> {state.errors.name && <span>{state.errors.name}</span>} <input name="email" value={state.values.email} onChange={handleChange} /> <textarea name="message" value={state.values.message} onChange={handleChange} /> <button disabled={state.isSubmitting}> {state.isSubmitting ? 'Отправка...' : 'Отправить'} </button> </form> );}useState vs useReducer
Заголовок раздела «useState vs useReducer»| Критерий | useState | useReducer |
|---|---|---|
| Количество полей | 1-2 | 3+ связанных |
| Логика обновления | Простая | Сложная, зависит от предыдущего |
| Тестирование | Тестируется компонент | Редюсер тестируется отдельно |
| Действия из компонента | Прямые вызовы | dispatch({ type }) |