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

React API

// Классовый компонент
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 (пока нет хука 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 показывает fallback пока дочерние компоненты “ждут” (lazy loading, data fetching).

import { lazy, Suspense } from 'react';
// Компонент загружается только когда нужен
const HeavyChart = lazy(() => import('./HeavyChart'));
function Dashboard() {
return (
<Suspense fallback={<div>Загрузка графика...</div>}>
<HeavyChart />
</Suspense>
);
}
// Ресурс, совместимый с 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>
);
}

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>

Позволяют вернуть несколько элементов без лишней обёртки:

// Длинный синтаксис — можно передать key
function 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>
))}
</>
);
}

Позволяет родительскому компоненту получить 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>
</>
);
}

Мемоизация компонента — пропускает ре-рендер если 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 у родителя
);
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;
}