Доки по разработке
This project is maintained by teniryte
Этот файл описывает более продвинутые возможности React: useReducer, useMemo, useCallback, хук use, контекст, императивные ref, flushSync, useSyncExternalStore, а также основы клиентской гидратации и простого Suspense.
Начиная с React 18, несколько вызовов setState в одном “тики” событий объединяются (батчатся) в один рендер. Это справедливо не только для обработчиков событий, но и для промисов, setTimeout и других асинхронных источников.
export function BatchingExample() {
const [count, setCount] = useState(0);
const [text, setText] = useState('hello');
function handleClick() {
// Эти два вызова обычно приведут к одному ререндеру
setCount(c => c + 1);
setText(t => t.toUpperCase());
}
return (
<div>
<button onClick={handleClick}>
Count: {count}
</button>
<div>{text}</div>
</div>
);
}
Важно понимать: setState асинхронен в том смысле, что переменная state не меняется мгновенно — новое значение появится только при следующем рендере.
useReducer: сложное и связанное состояниеuseReducer — альтернатива useState для случаев, когда:
Редьюсер — это чистая функция: она не должна иметь побочных эффектов и при одинаковых аргументах всегда возвращает один и тот же результат.
type CounterState = { count: number };
type CounterAction =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset'; payload?: number };
function counterReducer(state: CounterState, action: CounterAction): CounterState {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: action.payload ?? 0 };
default:
return state;
}
}
// Ленивая инициализация
function createInitialState(initialCount: number): CounterState {
console.log('Создаём начальное состояние');
return { count: initialCount };
}
export function CounterWithReducer() {
const [state, dispatch] = useReducer(counterReducer, 0, createInitialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'reset', payload: 10 })}>Reset to 10</button>
</div>
);
}
Рекомендация: не помещайте сайд‑эффекты внутрь редьюсера, вместо этого реагируйте на изменения состояния с помощью useEffect.
useMemo и useCallbackuseMemo: кеширование дорогих вычисленийuseMemo возвращает мемоизированное значение: пока массив зависимостей не изменился, результат не будет пересчитываться. Нужен только там, где вычисление действительно тяжёлое.
export function FilterList({ items }: { items: string[] }) {
const [query, setQuery] = useState('');
const filtered = useMemo(() => {
// имитация тяжёлой операции
const start = performance.now();
while (performance.now() - start < 5) {}
return items.filter(item =>
item.toLowerCase().includes(query.toLowerCase()),
);
}, [items, query]);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Поиск..."
/>
<ul>
{filtered.map(item => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}
useCallback: стабильные обработчикиuseCallback мемоизирует функцию, чтобы дочерние компоненты не получали новый колбэк при каждом рендере (что важно при использовании React.memo).
const IncrementButton = React.memo(function IncrementButton({
onClick,
}: {
onClick: () => void;
}) {
console.log('Render button');
return <button onClick={onClick}>Increment</button>;
});
export function CallbackExample() {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount(c => c + 1);
}, []);
return (
<div>
<p>Count: {count}</p>
<IncrementButton onClick={increment} />
</div>
);
}
use для промисов и контекстаХук use позволяет “распаковать” промис или контекст прямо внутри компонента, без await и без отдельных useEffect. Обычно используется в сочетании с Suspense.
Идея:
use(promise);<Suspense>.async function fetchUser(id: number) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error('Failed to load');
return res.json() as Promise<{ id: number; name: string }>;
}
const userPromise = fetchUser(1);
function UserInfo() {
const user = use(userPromise);
return <div>User: {user.name}</div>;
}
export function UserPage() {
return (
<Suspense fallback={<div>Загрузка...</div>}>
<UserInfo />
</Suspense>
);
}
Контекст позволяет “пробрасывать” данные через несколько уровней дерева без пропсов. Современный подход: создать контекст и обёртку‑хук, который проверяет, что компонент находится внутри провайдера.
type User = { name: string; age: number } | null;
type UserContextValue = {
user: User;
setUser: (user: User) => void;
};
const UserContext = createContext<UserContextValue | null>(null);
export function UserProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User>(null);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
export function useUser() {
const ctx = useContext(UserContext);
if (!ctx) {
throw new Error('useUser должен вызываться внутри UserProvider');
}
return ctx;
}
export function UserPanel() {
const { user, setUser } = useUser();
return (
<div>
<h2>User</h2>
{user ? (
<>
<div>Name: {user.name}</div>
<div>Age: {user.age}</div>
</>
) : (
<div>No user</div>
)}
<button onClick={() => setUser({ name: 'Denis', age: 34 })}>
Set user
</button>
<button onClick={() => setUser(null)}>Clear</button>
</div>
);
}
useImperativeHandle и императивные методыИногда нужно дать родителю императивный интерфейс к дочернему компоненту (например, метод focus, scrollToTop, open и т.п.). Для этого используют комбинацию forwardRef и useImperativeHandle.
type ColorHandle = {
setColor: (color: string) => void;
};
const ColorBox = forwardRef<ColorHandle, { initial?: string }>(
function ColorBox({ initial = 'white' }, ref) {
const [color, setColor] = useState(initial);
useImperativeHandle(ref, () => ({
setColor(newColor: string) {
setColor(newColor);
},
}));
return (
<div style={{ width: 100, height: 100, background: color }} />
);
},
);
export function ImperativeExample() {
const ref = useRef<ColorHandle | null>(null);
return (
<div>
<ColorBox ref={ref} initial="lightgray" />
<button onClick={() => ref.current?.setColor('red')}>Red</button>
<button onClick={() => ref.current?.setColor('green')}>Green</button>
</div>
);
}
Этот паттерн полезен для интеграции с canvas, картами, кастомными виджетами, но им не стоит злоупотреблять — он нарушает чисто декларативный стиль.
flushSync для синхронных обновленийИногда необходимо, чтобы обновление состояния произошло сразу в текущем кадре, без ожидания батчинга. Для этого в react-dom есть утилита flushSync, которая временно отключает планировщик и заставляет React синхронно обновить DOM.
Типовые случаи:
import { flushSync } from 'react-dom';
export function FlushSyncExample() {
const [showInput, setShowInput] = useState(false);
const inputRef = useRef<HTMLInputElement | null>(null);
const toggle = () => {
flushSync(() => {
setShowInput(prev => !prev);
});
// к этому моменту DOM уже обновлён
inputRef.current?.focus();
};
return (
<div>
<button onClick={toggle}>Toggle input</button>
{showInput && (
<input ref={inputRef} placeholder="Фокус сразу здесь" />
)}
</div>
);
}
Используйте flushSync очень точечно: он может сильно ударить по производительности, если в блоке много обновлений.
useSyncExternalStore: работа с внешними сторамиuseSyncExternalStore — специальный хук для подписки на внешние источники данных (вне React): объекты, глобальные сторы, WebSocket, события окна и т.д. Он гарантирует корректную работу в concurrent‑режиме и отсутствие “разрывов” (tearing).
Нужно передать три функции:
subscribe(listener) — подписка и возврат функции отписки;getSnapshot — как получить актуальное значение на клиенте;getServerSnapshot — как получить значение во время SSR.function useOnlineStatus() {
const subscribe = (callback: () => void) => {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
};
const getSnapshot = () => navigator.onLine;
const getServerSnapshot = () => true; // допустимое значение по умолчанию
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}
export function OnlineIndicator() {
const isOnline = useOnlineStatus();
return (
<div>
Статус: {isOnline ? 'online' : 'offline'}
</div>
);
}
useSyncExternalStoreЧастый паттерн — создать функцию, которая на основе какого‑то источника (например, matchMedia) возвращает хук для подписки на него.
function makeMediaQueryHook(query: string) {
return function useMediaQuery() {
return useSyncExternalStore(
callback => {
const media = window.matchMedia(query);
media.addEventListener('change', callback);
return () => media.removeEventListener('change', callback);
},
() => window.matchMedia(query).matches,
() => false,
);
};
}
const useNarrowScreen = makeMediaQueryHook('(max-width: 600px)');
export function MediaExample() {
const isNarrow = useNarrowScreen();
return (
<div>
{isNarrow ? 'Мобильный режим' : 'Десктопный режим'}
</div>
);
}
Такой подход позволяет один раз описать логику подписки, а затем переиспользовать её в разных компонентах.
При серверном рендеринге React сначала генерирует HTML на сервере, а потом “привязывает” к нему обработчики событий на клиенте — этот процесс называется гидратацией.
Вручную это выглядит так:
import { createRoot, hydrateRoot } from 'react-dom/client';
import { renderToString } from 'react-dom/server';
function App() {
return <button onClick={() => alert('Click')}>Click me</button>;
}
// На сервере
const html = renderToString(<App />);
// На клиенте
const container = document.getElementById('root')!;
container.innerHTML = html;
// Привязать обработчики к уже существующему HTML:
hydrateRoot(container, <App />);
В фреймворках (Next.js и др.) этот процесс скрыт внутри, но понимание принципа помогает диагностировать ошибки “Text content does not match server render”.
lazySuspense — это механизм отложенной отрисовки: пока часть дерева “занята” (например, загружает данные или код), React показывает fallback.
Комбинация React.lazy + Suspense — базовый паттерн ленивой загрузки компонентов.
const UsersList = React.lazy(() =>
import('./UsersList').then(mod => ({ default: mod.UsersList })),
);
async function fetchUsers() {
const res = await fetch('/api/users');
if (!res.ok) throw new Error('Failed to load');
return res.json() as Promise<Array<{ id: number; name: string }>>;
}
function UsersSection() {
const users = use(fetchUsers());
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
export function UsersPage() {
return (
<Suspense fallback={<div>Загрузка пользователей...</div>}>
<h1>Users</h1>
<UsersList />
{/* или напрямую <UsersSection /> */}
</Suspense>
);
}
Дальнейшее развитие темы Suspense — работа с кэшем промисов, nested Suspense, transitions и оптимистичными обновлениями.