Перейти к содержимому

useRef, useContext, useReducer

useRef возвращает мутабельный объект { current: T }, который сохраняется между рендерами. Изменение ref.current не вызывает рендер.

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

Ограничивает что родитель видит через 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>
</>
);
}

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 — полноценная замена 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>
);
}
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>
);
}
КритерийuseStateuseReducer
Количество полей1-23+ связанных
Логика обновленияПростаяСложная, зависит от предыдущего
ТестированиеТестируется компонентРедюсер тестируется отдельно
Действия из компонентаПрямые вызовыdispatch({ type })