По мере усложнения React-приложений паттерны, которые казались подходящими, когда вы только начинали, начинают казаться ограниченными. Возможно, вы создали успешный MVP (минимально жизнеспособный продукт), но теперь замечаете едва заметные проблемы с производительностью. Или, возможно, вы запутались с управлением состояниями, а логика получения данных превратилась в нечто неузнаваемое.
Такое случается со всеми при переходе от низшего к среднему или продвинутому уровню работы с React. Хорошая новость заключается в том, что есть продвинутые техники, которые помогут упростить сложные проблемы. В этой статье мы рассмотрим 15 таких приемов, начиная с умного использования useCallback и ref и заканчивая применением Suspense для получения данных, экспериментами с виртуализацией, улучшением способов обработки ошибок, оптимизацией производительности и многим другим.
Поначалу эти техники могут показаться пугающими, но я буду придерживаться доступного подхода. К концу статьи у вас уже будет богатый набор инструментов, который вы сможете использовать, когда ваша кодовая база (и карьера!) столкнется с более сложными проблемами.
1. Применение useCallback со ссылкой на постоянный сервис
Мы часто видим, как useCallback используется для мемоизации встроенных стрелочных функций в обработчиках событий. Это делается для того, чтобы ссылка на функцию оставалась стабильной и не вызывала ненужных повторных рендерингов при передаче в качестве свойства.
Но по мере своего продвижения вы обнаружите, что можете использовать данную технику для поддержания стабильных ссылок на более сложные сервисы — веб-сокеты, воркеры или другие постоянные ресурсы — чтобы они не создавались без необходимости при каждом рендеринге.
Этот подход основывается на useRef и useCallback: вы обеспечиваете стабильность соединения с долгоживущим сервисом. Это позволяет сэкономить на производительности и избежать непреднамеренных повторных подключений.
Пример:
function createExpensiveService() {
// Представьте, что это используется для установки веб-сокета или общего воркера
return { send: (msg) => console.log('Sending:', msg) };
}
function usePersistentService() {
const serviceRef = React.useRef(createExpensiveService());
// Мемоизируйте функцию отправки, чтобы она никогда не менялась
const stableSend = React.useCallback((msg) => {
serviceRef.current.send(msg);
}, []);
return stableSend;
}
function MyComponent() {
const send = usePersistentService();
return <button onClick={() => send('HELLO')}>Send Message</button>;
}
2. Использование Ref вместо состояния для простоты
Иногда мы совершаем ошибку, помещая все изменяющиеся данные в состояние. Но бывают ситуации, когда вам просто нужно значение, которое не вызовет повторного рендеринга. В таких случаях проще и эффективнее использовать ref.
Представьте счетчик, который нужно просто считывать и обновлять в рамках системы, не затрагивая пользовательский интерфейс. ref — идеальный вариант в это случае. Не нужно ни useState, ни сложных рендерингов — достаточно иметь стабильный «блок» для хранения изменяющегося значения.
Пример:
function MyCounter() {
const counterRef = React.useRef(0);
const increment = () => {
counterRef.current++;
console.log('Ref count is now:', counterRef.current);
};
return <button onClick={increment}>Increment (Check Console)</button>;
}
3. Использование Suspense для получения данных с задействованием глобального кэша ресурсов
Во многих приложениях React получение данных связано с useEffect, загрузкой состояний и множеством ручных проверок. Suspense способен упростить все это, позволяя компонентам «читать» из специального ресурса данных. Если данные не готовы, компонент автоматически приостанавливается, а React показывает резервный пользовательский интерфейс до тех пор, пока данные не поступят. Такой подход централизует логику загрузки, делает ваши компоненты чище и больше фокусируется на рендеринге.
Пример (концептуальный):
function createResource(fetchFn) {
let status = 'pending';
let result;
const promise = fetchFn().then(
data => { status = 'success'; result = data; },
err => { status = 'error'; result = err; }
);
return {
read() {
if (status === 'pending') throw promise;
if (status === 'error') throw result;
return result;
}
};
}
const userResource = createResource(() => fetch('/api/user').then(r => r.json()));
function UserProfile() {
const user = userResource.read();
return <div>Hello, {user.name}!</div>;
}
function App() {
return (
<React.Suspense fallback={<div>Loading user data...</div>}>
<UserProfile />
</React.Suspense>
);
}
4. Использование Suspense вместе с динамическим импортом и подсказками предварительной загрузки
Разделение кода — обычное дело для React.lazy(), но вы можете пойти дальше, предварительно загрузив код до того, как он понадобится пользователю. Это сократит время ожидания, когда пользователь наконец нажмет на кнопку или перейдет по определенному маршруту. Начните загрузку «тяжелых» компонентов в фоновом режиме, чтобы они были сразу же готовы, когда это потребуется.
Пример:
const HeavyChart = React.lazy(() => import('./HeavyChart'));
function usePreloadHeavyChart() {
React.useEffect(() => {
import('./HeavyChart'); // запуск предварительной загрузки при монтировании
}, []);
}
function Dashboard() {
usePreloadHeavyChart();
return (
<React.Suspense fallback={<div>Loading Chart...</div>}>
<HeavyChart />
</React.Suspense>
);
}
5. Границы ошибок с автоматическим повтором
В какой-то момент что-то в вашем приложении может дать сбой; возможно, это будет сетевой запрос или лениво загруженный компонент. Традиционные границы ошибок показывают запасной вариант UI и на этом останавливаются. Усовершенствовав их, вы можете попробовать запустить автоматическое восстановление, повторив попытку после небольшой задержки. Это значительно улучшит пользовательский опыт, превратив временный сбой в плавное восстановление.
Пример:
class AutoRetryErrorBoundary extends React.Component {
state = { hasError: false, attempt: 0 };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidUpdate() {
if (this.state.hasError) {
setTimeout(() => {
this.setState(s => ({ hasError: false, attempt: s.attempt + 1 }));
}, 2000);
}
}
render() {
if (this.state.hasError) return <div>Retrying...</div>;
return this.props.children(this.state.attempt);
}
}
function UnstableComponent({ attempt }) {
if (attempt < 2) throw new Error('Simulated Crash!');
return <div>Loaded on attempt {attempt}</div>;
}
// Использование
<AutoRetryErrorBoundary>
{(attempt) => <UnstableComponent attempt={attempt} />}
</AutoRetryErrorBoundary>
6. Виртуализация списков с динамической высотой элементов
При наличии огромных списков рендеринг каждого элемента может привести к снижению производительности. Библиотеки виртуализации (например, react-window) отображают только то, что видно. Но что делать, если элементы имеют непредсказуемую высоту? Их можно измерять динамически и передавать эти измерения обратно в логику виртуализации. Это сокращает потребление памяти и время рендеринга, обеспечивая плавность прокрутки.
Пример:
import { VariableSizeList as List } from 'react-window';
function useDynamicMeasurement(items) {
const sizeMap = React.useRef({});
const refCallback = index => el => {
if (el) {
const height = el.getBoundingClientRect().height;
sizeMap.current[index] = height;
}
};
const getSize = index => sizeMap.current[index] || 50;
return { refCallback, getSize };
}
function DynamicHeightList({ items }) {
const { refCallback, getSize } = useDynamicMeasurement(items);
return (
<List height={400} itemCount={items.length} itemSize={getSize} width={300}>
{({ index, style }) => (
<div style={style} ref={refCallback(index)}>
{items[index]}
</div>
)}
</List>
);
}
7. Использование машины состояний для сложных потоков пользовательского интерфейса
Когда ваш компонент начинает напоминать спагетти из операторов if, на помощь придет машина состояний. Такие инструменты, как XState, прекрасно интегрируются с хуками React. Вместо того чтобы управлять множеством булевых значений, вы определяете состояния и переходы в рамках единой, чистой диаграммы. Речь идет о смене ментальной модели: вы составляете «карту» работы вашего пользовательского интерфейса, что облегчает понимание и отладку.
Пример:
import { useMachine } from '@xstate/react';
import { createMachine } from 'xstate';
const formMachine = createMachine({
initial: 'editing',
states: {
editing: { on: { SUBMIT: 'validating' } },
validating: {
invoke: {
src: 'validateForm',
onDone: 'success',
onError: 'error'
}
},
success: {},
error: {}
}
});
function Form() {
const [state, send] = useMachine(formMachine, {
services: { validateForm: async () => {/* логика валидации */} }
});
return (
<button onClick={() => send('SUBMIT')}>
{state.value === 'editing' ? 'Submit' : state.value.toString()}
</button>
);
}
8. Управление параллелизмом с помощью useTransition и очередей задач
В React 18 появилась функция useTransition, позволяющая помечать определенные обновления состояния как «несрочные». Это может стать решающим фактором для производительности при высокой нагрузке. Представьте, что получаете большие объемы данных или выполняете дорогостоящие вычисления. Откладывая несрочные обновления, вы сохраняете отзывчивость пользовательского интерфейса и избегаете блокировки главного потока.
Пример:
function ComplexUI() {
const [isPending, startTransition] = React.useTransition();
const [data, setData] = React.useState([]);
function loadMore() {
startTransition(() => {
setData(old => [...old, ...generateMoreData()]);
});
}
return (
<>
<button onClick={loadMore}>Load More</button>
{isPending && <span>Loading more data...</span>}
<List data={data} />
</>
);
}
9. Применение useImperativeHandle для создания API управляемых компонентов
Иногда нужно, чтобы родительский компонент напрямую управлял дочерним. В качестве примера можно привести вызов childRef.current.focus() для пользовательского ввода. useImperativeHandle — это хук, который позволяет определить, что видит родитель, когда использует ref в дочернем компоненте. Он идеально подходит для создания аккуратных, контролируемых API компонентов, которые напоминают вызов метода в экземпляре класса, но в удобном для React формате.
Пример:
const FancyInput = React.forwardRef((props, ref) => {
const inputRef = React.useRef();
React.useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
getValue: () => inputRef.current.value
}));
return <input ref={inputRef} {...props} />;
});
function Parent() {
const fancyRef = React.useRef();
return (
<>
<FancyInput ref={fancyRef} />
<button onClick={() => fancyRef.current.focus()}>Focus Input</button>
</>
);
}
10. Прогрессивная гидратация с помощью пользовательского хука
Рендеринг на стороне сервера (SSR) позволяет быстро донести контент до пользователя, но одновременная гидратация огромного приложения может замедлить процесс интерактивности. Отложив гидратацию некритичных частей страницы, вы сохраните скорость начальной загрузки. Пользовательский хук, который постепенно гидратирует определенные компоненты после задержки, сделает SSR еще более плавным.
Пример:
function useProgressiveHydration(delay = 1000) {
const [hydrated, setHydrated] = React.useState(false);
React.useEffect(() => {
const t = setTimeout(() => setHydrated(true), delay);
return () => clearTimeout(t);
}, [delay]);
return hydrated;
}
function HeavyComponent() {
const hydrated = useProgressiveHydration();
return hydrated ? <ExpensiveTree /> : <Placeholder />;
}
11. Сочетание порталов с Suspense для тяжелых модалов
Модалы часто бывают большими и сложными. Вместо того чтобы загружать их заранее и раздувать пакет, можете применять для них ленивую загрузку. Комбинируйте эту технику с React-порталами для рендеринга модала вне основной структуры DOM, сохраняя чистоту и должную многоуровневость. Suspense обрабатывает состояния загрузки, и у пользователя не возникает ощущения, что он имеет дело с громоздким кодом.
Пример:
const HeavyModal = React.lazy(() => import('./HeavyModal'));
function PortalModal({ open }) {
return open
? ReactDOM.createPortal(
<React.Suspense fallback={<div>Loading Modal...</div>}>
<HeavyModal />
</React.Suspense>,
document.body
)
: null;
}
12. Использование useLayoutEffect для плавного измерения макета
useEffect запускается после отрисовки браузера, что вполне подходит для большинства случаев. Но если вам нужно измерить расположение (например, считать положение или высоту элемента) и немедленно подправить что-то, пока пользователь этого не увидел, useLayoutEffect станет вашим другом. Он запускается сразу после того, как React обновляет DOM, но перед отрисовкой, что гарантирует отсутствие мерцания.
Пример:
function Popover({ anchorRef }) {
const popoverRef = React.useRef();
React.useLayoutEffect(() => {
const anchorRect = anchorRef.current.getBoundingClientRect();
popoverRef.current.style.top = `${anchorRect.bottom}px`;
}, [anchorRef]);
return <div ref={popoverRef} className="popover">Content</div>;
}
13. Использование API React Profiler для динамических настроек производительности
Знаете ли вы, что в React есть API Profiler, который можно использовать в продакшене для измерения времени рендеринга? Вы можете обнаруживать медленные компоненты и динамически корректировать стратегии — например, увеличивать пороговые значения мемоизации или откладывать определенные обновления — на основе показателей реального времени. Вместо того чтобы гадать о том, где искать узкие места в производительности, вы проводите измерения и адаптируетесь под ситуацию.
Пример:
import { unstable_Profiler as Profiler } from 'react';
function DynamicTuner() {
const [threshold, setThreshold] = React.useState(10);
function onRender(id, phase, actualDuration) {
if (actualDuration > threshold) {
// Корректировка стратегии на основе производительности
setThreshold(t => t + 5);
}
}
return (
<Profiler id="App" onRender={onRender}>
<MyAppComponents />
</Profiler>
);
}
14. Использование useReducer с Immer для управления неизменяемыми состояниями
Управление глубоко вложенным состоянием может стать головной болью. Использование useReducer обеспечивает предсказуемость обновлений, а сочетание его с библиотекой Immer упрощает работу с неизменяемыми состояниями. Вместо того чтобы самостоятельно тщательно клонировать объекты, вы можете написать «мутирующий» код внутри produce(), который приведет к чистому, неизменяемому обновлению. Это позволяет следить за простотой редьюсеров и уменьшить количество ошибок.
Пример:
import produce from 'immer';
function reducer(state, action) {
return produce(state, draft => {
if (action.type === 'ADD_ITEM') {
draft.items.push(action.payload);
}
});
}
function ComplexList() {
const [state, dispatch] = React.useReducer(reducer, { items: [] });
return (
<button onClick={() => dispatch({ type: 'ADD_ITEM', payload: 'X' })}>
Add Item
</button>
);
}
15. Эффективный рендеринг больших элементов SVG/Canvas с помощью окон и порталов
Если ваше приложение имеет дело с массивными диаграммами, картами или графами, вы управляете огромными элементами SVG или Canvas, которые замедляют рендеринг. Можете разбить эти большие визуализации на части и рендерить только отдельные части за раз. Это схоже с тем, как используется виртуализация для списков. Используйте порталы и технологии окон, чтобы разбить большой объем графики на управляемые фрагменты и не перегружать браузер.
function Segment({ color, target }) {
return ReactDOM.createPortal(
<svg width="200" height="100"><rect width="200" height="100" fill={color} /></svg>,
target
);
}
export default function App() {
const containerRef = useRef(null);
const [visible, setVisible] = useState([0,1]); // какие сегменты видимы
useEffect(() => {
const onScroll = () => {
const start = Math.floor(containerRef.current.scrollTop / 100);
setVisible([start, start+1]);
};
containerRef.current.addEventListener('scroll', onScroll);
return () => containerRef.current.removeEventListener('scroll', onScroll);
}, []);
return (
<div ref={containerRef} style={{ height:200, overflowY:'auto' }}>
{[...Array(10)].map((_, i) => <div key={i} id={'seg'+i} style={{height:100}} />)}
{visible.map(i => <Segment key={i} color={i%2?'blue':'green'} target={document.getElementById('seg'+i)} />)}
</div>
);
}
Резюме
Эти техники не предназначены для использования по методу «все и сразу», и уж точно они не подойдут для каждой кодовой базы. Описанные продвинутые инструменты станут полезными по мере роста ваших приложений и навыков. Рассмотрите эти подходы, когда столкнетесь с ограничениями более простых паттернов.
Читайте также:
- Зачем использовать RTK Query для API-вызовов в React
- Компоненты высшего порядка в React Virtualized
- 9 оптимальных библиотек компонентов React на 2025 год
Читайте нас в Telegram, VK и Дзен
Перевод статьи Mate Marschalko: 18 Advanced React Techniques Every Senior Dev Needs to Know





