React API
Функциональные vs Классовые компоненты
Заголовок раздела «Функциональные vs Классовые компоненты»// Классовый компонентclass Counter extends React.Component<{}, { count: number }> { state = { count: 0 };
render() { return ( <button onClick={() => this.setState({ count: this.state.count + 1 })}> {this.state.count} </button> ); }}
// Функциональный компонент — эквивалентfunction Counter() { const [count, setCount] = useState(0); return ( <button onClick={() => setCount(c => c + 1)}> {count} </button> );}Ключевые отличия:
- Класс:
this.state, lifecycle методы (componentDidMount,componentDidUpdate,componentWillUnmount) - Функция: Hooks (
useState,useEffect), проще читать и тестировать - Новые фичи React (Suspense, Concurrent Mode) лучше работают с функциональными компонентами
Error Boundaries
Заголовок раздела «Error Boundaries»Error Boundaries перехватывают ошибки в дочернем дереве. Только классовые компоненты могут быть Error Boundaries (пока нет хука useErrorBoundary).
interface ErrorBoundaryState { hasError: boolean; error: Error | null;}
class ErrorBoundary extends React.Component< React.PropsWithChildren<{ fallback: React.ReactNode }>, ErrorBoundaryState> { state: ErrorBoundaryState = { hasError: false, error: null };
static getDerivedStateFromError(error: Error): ErrorBoundaryState { return { hasError: true, error }; }
componentDidCatch(error: Error, info: React.ErrorInfo) { console.error('Error caught:', error, info.componentStack); }
render() { if (this.state.hasError) { return this.props.fallback; } return this.props.children; }}
// Использованиеfunction App() { return ( <ErrorBoundary fallback={<div>Что-то пошло не так</div>}> <BuggyComponent /> </ErrorBoundary> );}Suspense
Заголовок раздела «Suspense»Suspense показывает fallback пока дочерние компоненты “ждут” (lazy loading, data fetching).
Lazy loading компонентов
Заголовок раздела «Lazy loading компонентов»import { lazy, Suspense } from 'react';
// Компонент загружается только когда нуженconst HeavyChart = lazy(() => import('./HeavyChart'));
function Dashboard() { return ( <Suspense fallback={<div>Загрузка графика...</div>}> <HeavyChart /> </Suspense> );}Suspense + Data Fetching (React 18)
Заголовок раздела «Suspense + Data Fetching (React 18)»// Ресурс, совместимый с Suspense (через библиотеку или вручную)function createResource<T>(promise: Promise<T>) { let status: 'pending' | 'success' | 'error' = 'pending'; let result: T; let error: unknown;
promise.then( data => { status = 'success'; result = data; }, err => { status = 'error'; error = err; } );
return { read(): T { if (status === 'pending') throw promise; // Suspense "поймает" if (status === 'error') throw error; return result!; } };}
const userResource = createResource(fetchUser(1));
function UserProfile() { const user = userResource.read(); // бросает Promise если не загружен return <div>{user.name}</div>;}
function App() { return ( <Suspense fallback={<Spinner />}> <UserProfile /> </Suspense> );}Portals
Заголовок раздела «Portals»Portal рендерит дочерний элемент в другой DOM-узел, вне иерархии родителя. Идеально для модальных окон, тултипов, дропдаунов.
import { createPortal } from 'react-dom';
interface ModalProps { isOpen: boolean; onClose: () => void; children: React.ReactNode;}
function Modal({ isOpen, onClose, children }: ModalProps) { if (!isOpen) return null;
// Рендерится в #modal-root, но события всплывают через React-дерево return createPortal( <div className="modal-overlay" onClick={onClose}> <div className="modal-content" onClick={e => e.stopPropagation()}> {children} </div> </div>, document.getElementById('modal-root')! );}
// В HTML: <div id="modal-root"></div>Fragments
Заголовок раздела «Fragments»Позволяют вернуть несколько элементов без лишней обёртки:
// Длинный синтаксис — можно передать keyfunction List({ items }: { items: Array<{ id: number; name: string }> }) { return ( <> {items.map(item => ( <React.Fragment key={item.id}> <dt>{item.id}</dt> <dd>{item.name}</dd> </React.Fragment> ))} </> );}forwardRef
Заголовок раздела «forwardRef»Позволяет родительскому компоненту получить ref на DOM-элемент внутри дочернего:
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { label: string;}
const LabeledInput = React.forwardRef<HTMLInputElement, InputProps>( ({ label, ...props }, ref) => ( <label> {label} <input ref={ref} {...props} /> </label> ));
LabeledInput.displayName = 'LabeledInput';
// Использованиеfunction Form() { const inputRef = useRef<HTMLInputElement>(null);
return ( <> <LabeledInput ref={inputRef} label="Email" type="email" /> <button onClick={() => inputRef.current?.focus()}> Фокус на поле </button> </> );}React.memo
Заголовок раздела «React.memo»Мемоизация компонента — пропускает ре-рендер если props не изменились:
interface ListItemProps { id: number; text: string; onClick: (id: number) => void;}
// Без memo — перерисовывается при каждом рендере родителя// С memo — только если id, text или onClick изменилисьconst ListItem = React.memo(function ListItem({ id, text, onClick }: ListItemProps) { console.log(`render ${id}`); return <li onClick={() => onClick(id)}>{text}</li>;});
// Кастомное сравнение (опционально)const ListItem2 = React.memo( function ListItem2({ id, text, onClick }: ListItemProps) { return <li onClick={() => onClick(id)}>{text}</li>; }, (prev, next) => prev.id === next.id && prev.text === next.text // onClick не сравниваем — используем useCallback у родителя);createContext + useContext
Заголовок раздела «createContext + useContext»interface ThemeContextValue { theme: 'light' | 'dark'; toggle: () => void;}
const ThemeContext = React.createContext<ThemeContextValue | null>(null);
function ThemeProvider({ children }: React.PropsWithChildren) { const [theme, setTheme] = useState<'light' | 'dark'>('light'); const toggle = useCallback(() => { setTheme(t => t === 'light' ? 'dark' : 'light'); }, []);
return ( <ThemeContext.Provider value={{ theme, toggle }}> {children} </ThemeContext.Provider> );}
function useTheme() { const ctx = useContext(ThemeContext); if (!ctx) throw new Error('useTheme must be used within ThemeProvider'); return ctx;}