useDeferredValue React 19: как Scheduler меняет правила игры с частыми обновлениями
Раньше задача управления частыми обновлениями в React решалась через lodash.debounce или кастомные хуки с риском протухших замыканий. В production это приводило к потере ввода, лагам интерфейса и спаму сетевых запросов. React 19 меняет подход: useDeferredValue в связке с Scheduler сохраняет все обновления, но расставляет приоритеты.
Как это работает
В отличие от debounce, который отбрасывает лишние вызовы, useDeferredValue не теряет данные. Он разделяет обновления на срочные (ввод текста) и фоновые (тяжелые вычисления). React сам решает, когда выполнить вторые, используя механизм приоритетов Scheduler. По умолчанию задержка до 300 мс, регулируемая через Scheduler.unstable_shouldYield.
Пример для поиска с тяжелой фильтрацией:
Здесь query обновляется мгновенно — инпут не тормозит. A deferredQuery React подтягивает позже, сохраняя отзывчивость.
Где это критически важно
useDeferredValue незаменим, когда все изменения важны и их нельзя терять. Типичный production-сценарий: поиск с автодополнением, где каждое нажатие клавиши должно быть учтено, но фильтрация результатов — тяжелая операция. Или рендер списка с вложенными компонентами при скролле.
Типичная ошибка
Начинающие разработчики применяют useDeferredValue для контроля сетевых запросов. Это не работает: если пользователь кликает каждые 100 мс, на сервер полетит столько же запросов. useDeferredValue не сбрасывает их и не отменяет — он только откладывает рендер. Для API-вызовов все еще нужен debounce.
Когда debounce остается
Старый добрый debounce незаменим в трех случаях: строгий тайминг после последнего действия (отправка формы, автосохранение), сброс предыдущих запросов, и контроль количества вызовов API. useDeferredValue для этого не подходит.
Вывод: useDeferredValue — это не замена debounce, а новый инструмент для UI-оптимизаций внутри компонента, который требует осознанного выбора между потерей данных и отзывчивостью.
Раньше задача управления частыми обновлениями в React решалась через lodash.debounce или кастомные хуки с риском протухших замыканий. В production это приводило к потере ввода, лагам интерфейса и спаму сетевых запросов. React 19 меняет подход: useDeferredValue в связке с Scheduler сохраняет все обновления, но расставляет приоритеты.
Как это работает
В отличие от debounce, который отбрасывает лишние вызовы, useDeferredValue не теряет данные. Он разделяет обновления на срочные (ввод текста) и фоновые (тяжелые вычисления). React сам решает, когда выполнить вторые, используя механизм приоритетов Scheduler. По умолчанию задержка до 300 мс, регулируемая через Scheduler.unstable_shouldYield.
Пример для поиска с тяжелой фильтрацией:
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => {
return heavySearch(deferredQuery);
}, [deferredQuery]);Здесь query обновляется мгновенно — инпут не тормозит. A deferredQuery React подтягивает позже, сохраняя отзывчивость.
Где это критически важно
useDeferredValue незаменим, когда все изменения важны и их нельзя терять. Типичный production-сценарий: поиск с автодополнением, где каждое нажатие клавиши должно быть учтено, но фильтрация результатов — тяжелая операция. Или рендер списка с вложенными компонентами при скролле.
Типичная ошибка
Начинающие разработчики применяют useDeferredValue для контроля сетевых запросов. Это не работает: если пользователь кликает каждые 100 мс, на сервер полетит столько же запросов. useDeferredValue не сбрасывает их и не отменяет — он только откладывает рендер. Для API-вызовов все еще нужен debounce.
Когда debounce остается
Старый добрый debounce незаменим в трех случаях: строгий тайминг после последнего действия (отправка формы, автосохранение), сброс предыдущих запросов, и контроль количества вызовов API. useDeferredValue для этого не подходит.
Вывод: useDeferredValue — это не замена debounce, а новый инструмент для UI-оптимизаций внутри компонента, который требует осознанного выбора между потерей данных и отзывчивостью.
👍6❤3👀2
WeakRef и FinalizationRegistry: как GC чистит тяжелые ресурсы за вас
Вы тянете в React-компонент кэш изображений, WebGL-текстуру или WebAssembly-модуль. Ручная очистка в return useEffect работает только если компонент размонтируется. Если ссылка утекла в замыкание — память забивается. Слабые ссылки решают это без ручного управления.
WeakRef — слабая ссылка без блокировки GC
WeakRef хранит ссылку на объект, но не мешает сборщику мусора удалить его. Когда объект собран — deref() возвращает undefined. Вы не блокируете память, а только следите, жив ли объект.
FinalizationRegistry — колбэк после сборки
Регистрируете callback, который вызывается строго после того, как GC собрал объект. Это для освобождения внешних ресурсов: закрыть сокет, отпустить текстуру, отменить подписку.
Пример: самовозобновляющийся кэш
Типичная ошибка и предупреждение
GC не детерминирован — FinalizationRegistry может вызваться через минуту, час или никогда (если браузер упадет). Не полагайтесь на него для критичных по времени очисток. Используйте только для тяжелых объектов: WebAssembly, бинарные данные, WebGL. Для простых JSON или строк — обычный Map с return useEffect, так быстрее и надежнее.
Вывод:
WeakRef и FinalizationRegistry дают инструмент для автоматической очистки памяти без микроменеджмента в useEffect, но требуют понимания, что GC не мгновенный.
Вы тянете в React-компонент кэш изображений, WebGL-текстуру или WebAssembly-модуль. Ручная очистка в return useEffect работает только если компонент размонтируется. Если ссылка утекла в замыкание — память забивается. Слабые ссылки решают это без ручного управления.
WeakRef — слабая ссылка без блокировки GC
WeakRef хранит ссылку на объект, но не мешает сборщику мусора удалить его. Когда объект собран — deref() возвращает undefined. Вы не блокируете память, а только следите, жив ли объект.
FinalizationRegistry — колбэк после сборки
Регистрируете callback, который вызывается строго после того, как GC собрал объект. Это для освобождения внешних ресурсов: закрыть сокет, отпустить текстуру, отменить подписку.
Пример: самовозобновляющийся кэш
const cache = new Map<string, WeakRef<object>>();
const registry = new FinalizationRegistry((key: string) => {
cache.delete(key);
});
function useHeavyData(key: string) {
const dataRef = useRef<object | null>(null);
useEffect(() => {
const cached = cache.get(key)?.deref();
if (cached) {
dataRef.current = cached;
return;
}
const newData = computeHeavy(key);
cache.set(key, new WeakRef(newData));
registry.register(newData, key);
dataRef.current = newData;
}, [key]);
return dataRef.current;
}
Типичная ошибка и предупреждение
GC не детерминирован — FinalizationRegistry может вызваться через минуту, час или никогда (если браузер упадет). Не полагайтесь на него для критичных по времени очисток. Используйте только для тяжелых объектов: WebAssembly, бинарные данные, WebGL. Для простых JSON или строк — обычный Map с return useEffect, так быстрее и надежнее.
Вывод:
WeakRef и FinalizationRegistry дают инструмент для автоматической очистки памяти без микроменеджмента в useEffect, но требуют понимания, что GC не мгновенный.
👍5👀2
Тысяча SVG-иконок ломают FPS. Что делать с панелью мониторинга?
Каждый
Виртуализация для списков
React-window или Intersection Observer рендерят только то, что в окне просмотра. Это спасает для скроллящихся панелей, но если у вас анимация на каждом элементе (пульсация, вращение), DOM все равно пересчитывает layout для видимых узлов. Предупреждение: виртуализация бесполезна, когда все элементы находятся в области видимости.
OffscreenCanvas для статики
Рисуйте SVG в Web Worker через
Клонирование узлов для повторяющихся шаблонов
Сценарий: панель с 50 датчиками, каждый — повторяющаяся группа из трех SVG. Не делайте innerHTML для каждого. Загрузите шаблон один раз, распарсьте через
Глубокое клонирование быстрее парсинга строки на каждый элемент. Минус: если SVG разные — не сработает.
Вывод: Выбор стратегии зависит от сценария: для прокрутки — виртуализация, для повторяющихся иконок — importNode, для динамической отрисовки множества элементов — OffscreenCanvas с Path2D.
Каждый
<svg> в DOM — это полноценный узел с собственным жизненным циклом, стилями и вычислением layout. При 1000 таких узлов память растет, FPS падает, а скролл превращается в слайд-шоу. Частая ошибка — пытаться решить проблему через один инструмент, не учитывая сценарий использования. Вот три подхода с реальными trade-offs.Виртуализация для списков
React-window или Intersection Observer рендерят только то, что в окне просмотра. Это спасает для скроллящихся панелей, но если у вас анимация на каждом элементе (пульсация, вращение), DOM все равно пересчитывает layout для видимых узлов. Предупреждение: виртуализация бесполезна, когда все элементы находятся в области видимости.
OffscreenCanvas для статики
Рисуйте SVG в Web Worker через
OffscreenCanvas. Transferable объект передается без копирования памяти. На выходе — bitmap для отрисовки. Идеально для статичных иконок: никакого DOM, никаких стилей. Но кликнуть по такому SVG не выйдет, и анимацию через CSS не повесить. Если нужен hover — придется самостоятельно считать координаты.Клонирование узлов для повторяющихся шаблонов
Сценарий: панель с 50 датчиками, каждый — повторяющаяся группа из трех SVG. Не делайте innerHTML для каждого. Загрузите шаблон один раз, распарсьте через
DOMParser и клонируйте через document.importNode:const template = await fetch('sensor.svg').then(r => r.text());
const parser = new DOMParser();
const doc = parser.parseFromString(template, 'image/svg+xml');
const clone = document.importNode(doc.documentElement, true);
container.appendChild(clone);Глубокое клонирование быстрее парсинга строки на каждый элемент. Минус: если SVG разные — не сработает.
Вывод: Выбор стратегии зависит от сценария: для прокрутки — виртуализация, для повторяющихся иконок — importNode, для динамической отрисовки множества элементов — OffscreenCanvas с Path2D.
👍5👎2👀2
display: contents и React: когда переиспользование контейнера ломает семантику, разметку и анимации — и как чинить
Универсальные компоненты-обертки вроде
Семантика и роль контейнера
Проблема:
Но если у контейнера есть
Анимации и неожиданное поведение
Где не работает и trade-offs
* Табличные элементы (
* Корневые элементы (
* ARIA-атрибуты на контейнере пропадают для доступности: если нужен
Практический совет
В production используй
Это уменьшает дублирование стилей и сохраняет семантику. Но анимированные элементы всегда оборачивай дополнительно.
Вывод:
Универсальные компоненты-обертки вроде
Box или Card — стандарт production-кода. Но они часто нарушают семантику HTML, ломают flex/grid layout и делают DOM-дерево невалидным. Решение — display: contents, но оно не так безобидно, как кажется.Семантика и роль контейнера
Проблема:
<ul> внутри <div> рендерит <li> не прямым потомком, что нарушает спецификацию. display: contents делает контейнер невидимым для браузера, а children «встраиваются» в родителя. Пример на TypeScript:function Container({ as: Tag = 'div', children }: { as?: string; children: ReactNode }) {
return <Tag style={{ display: 'contents' }}>{children}</Tag>
}Но если у контейнера есть
aria-label или role, они теряются для скринридеров — это частая ошибка в production.Анимации и неожиданное поведение
display: contents удаляет элемент из визуального дерева. Анимации на нем (opacity, transform) перестают работать. Дети отрисовываются мгновенно, без плавности. Чинить так:// Плохо: анимация на контейнере с contents
<div style={{ display: 'contents', opacity: 0 }}> // Не сработает
// Хорошо: обертка отдельно
<div style={{ display: 'contents' }}>
<div style={{ opacity: 0, transition: 'opacity 0.3s' }}>...</div>
</div>
Где не работает и trade-offs
* Табличные элементы (
<tr>, <td>) не поддерживают contents — верстка ломается.* Корневые элементы (
<html>, <body>) — не пытайтесь.* ARIA-атрибуты на контейнере пропадают для доступности: если нужен
aria-label для скринридера, выноси его на детей или используй role="presentation" отдельно.Практический совет
В production используй
display: contents только для контейнеров без собственных стилей и анимаций. Например, в layout-компоненте:function FlexItem({ children }: { children: ReactNode }) {
return <div style={{ display: 'contents' }}>{children}</div>
}Это уменьшает дублирование стилей и сохраняет семантику. Но анимированные элементы всегда оборачивай дополнительно.
Вывод:
display: contents — мощный инструмент для чистого DOM и семантики, но требует явного разделения стилей и анимаций, иначе ломает визуальное поведение и доступность.❤5👀4👍3
Типизация forwarded refs в React 19: баги с
React 19 сделал
1.
Если передать
TypeScript выдаст ошибку, так как не видит контекст Refs. Решение — оборачивать в
Это гарантирует корректную инференцию типов и совместимость с React 19.
2. Инфер типов в HOC с forwarded refs — теряется связь
Когда оборачиваешь компонент с
Без явного
3. Render-props с
Частая ошибка: TypeScript выводит
Вывод: В React 19 явная типизация forwarded refs с generic-типами в каждом слое — не опция, а необходимость для production-кода, иначе баги с инференцией ломают компоненты с HOC и render-props.
ref как prop, инфер типов в HOC и render-props с useImperativeHandleReact 19 сделал
ref обычным prop, что на первый взгляд упрощает API. Однако на практике разработчики часто сталкиваются с неочевидными багами типов, особенно в production-коде с обёртками. Основная ошибка — игнорировать явную типизацию в угоду кажущейся простоте.1.
ref как prop без forwardRef — неочевидный багЕсли передать
ref как обычный prop без forwardRef, TypeScript не поймёт, что это forwarded ref:function MyInput({ ref, ...props }: { ref: React.Ref<HTMLInputElement> }) {
return <input ref={ref} />;
}TypeScript выдаст ошибку, так как не видит контекст Refs. Решение — оборачивать в
forwardRef и явно указывать generic:const MyInput = forwardRef<HTMLInputElement, Props>((props, ref) => ...);
Это гарантирует корректную инференцию типов и совместимость с React 19.
2. Инфер типов в HOC с forwarded refs — теряется связь
Когда оборачиваешь компонент с
ref в HOC, типы часто ломаются. Типичная ошибка: забыть явно пробросить generic для ref:function withLogger<T extends object>(
Component: React.ForwardRefExoticComponent<T & { ref: React.Ref<HTMLDivElement> }>
) {
return forwardRef<HTMLDivElement, T>((props, ref) => {
console.log('Rendered');
return <Component {...props} ref={ref} />;
});
}
Без явного
ForwardRefExoticComponent TypeScript будет гадать, выведя неправильный тип ref. Практический совет: всегда указывай generic для forwarded ref в HOC.3. Render-props с
useImperativeHandle — неправильный тип экземпляраuseImperativeHandle в React 19 работает с forwarded refs, но типизация render-props проваливается, если не объявить интерфейс явно:interface ImperativeActions { focus: () => void; }
const Input = forwardRef<ImperativeActions, {}>((_, ref) => {
useImperativeHandle(ref, () => ({ focus: () => console.log('Focus') }));
return <input />;
});
function Outer({ children }: { children: (ref: React.RefObject<ImperativeActions>) => React.ReactNode }) {
const ref = useRef<ImperativeActions>(null);
return <>{children(ref)}</>;
}Частая ошибка: TypeScript выводит
ImperativeActions как any. Предупреждение: используй React.ElementRef<typeof Input> вместо ручного объявления, чтобы избежать рассинхронизации типов.Вывод: В React 19 явная типизация forwarded refs с generic-типами в каждом слое — не опция, а необходимость для production-кода, иначе баги с инференцией ломают компоненты с HOC и render-props.
❤3👍2👀2👎1
Глубокая типизация GraphQL-ответов без codegen: generics и conditional types в TypeScript
Codegen для GraphQL часто превращается в головную боль: настройка схемы, синхронизация версий, регенерация при каждом изменении. Но если вы используете Relay с фрагментами или работаете в микросервисе с часто меняющейся схемой, можно обойтись без codegen. Типичная ошибка — вручную писать типы для каждого запроса, что для вложенных структур с пагинацией превращается в копипасту.
Почему codegen не всегда нужен
Codegen добавляет слой абстракции, который требует CI/CD-интеграции и синхронизации с серверной схемой. В проектах с частыми изменениями или фрагментарной архитектурой Relay это может стать узким местом. Альтернатива — вывести типы из самого запроса через TypeScript.
Подход: generic + conditional type
Создаем маппер типов GraphQL на TypeScript и utility type, который рекурсивно "проходит" по структуре запроса, обнаруженной через
Акцент на trade-offs
Главный плюс — один дженерик на все запросы без codegen. Но это требует ручного поддержания
Типичные ошибки
* Забыть указать
* Использовать при сложных интерфейсах: conditional type не всегда корректно обрабатывает discriminated unions — это потребует дополнительных проверок.
* Не синхронизировать маппер: если схема меняется, типы рассинхронизируются, и вы получите ложные гарантии.
Практический совет
Применяйте этот подход для микросервисов с простой, стабильной схемой или когда codegen конфликтует с Relay-фрагментарной структурой. В production я так типизировал запросы в проекте, где схема менялась раз в неделю, а codegen падал из-за кэширования — это сэкономило время без потери безопасности типов.
Вывод: Generics и conditional types — это легковесная альтернатива codegen для типизации вложенных GraphQL-ответов, но она требует ручного управления маппером и подходит для схем ограниченного размера.
Codegen для GraphQL часто превращается в головную боль: настройка схемы, синхронизация версий, регенерация при каждом изменении. Но если вы используете Relay с фрагментами или работаете в микросервисе с часто меняющейся схемой, можно обойтись без codegen. Типичная ошибка — вручную писать типы для каждого запроса, что для вложенных структур с пагинацией превращается в копипасту.
Почему codegen не всегда нужен
Codegen добавляет слой абстракции, который требует CI/CD-интеграции и синхронизации с серверной схемой. В проектах с частыми изменениями или фрагментарной архитектурой Relay это может стать узким местом. Альтернатива — вывести типы из самого запроса через TypeScript.
Подход: generic + conditional type
Создаем маппер типов GraphQL на TypeScript и utility type, который рекурсивно "проходит" по структуре запроса, обнаруженной через
as const. Этот тип заменяет строки на реальные типы из схемы, обрабатывая вложенность и массивы.// Маппер типов схемы
type GraphQLTypeMap = {
ID: string;
String: string;
User: { id: string; name: string; posts: Post[] };
Post: { id: string; title: string; comments: Comment[] };
Comment: { id: string; text: string; author: User };
};
// Рекурсивный conditional type
type UnwrapGraphQLResponse<T> = T extends { __typename: infer Name }
? T & GraphQLTypeMap[Name extends keyof GraphQLTypeMap ? Name : never]
: T extends (infer U)[]
? UnwrapGraphQLResponse<U>[]
: T;
// Запрос с as const
const query = {
user: {
id: true,
name: true,
posts: {
title: true,
comments: {
text: true,
author: { name: true }
}
}
}
} as const;
// Тип ответа выводится автоматически
type Response = UnwrapGraphQLResponse<typeof query>;
Акцент на trade-offs
Главный плюс — один дженерик на все запросы без codegen. Но это требует ручного поддержания
GraphQLTypeMap, что для схем с 50+ типами становится трудоемким. Для union/interface типов conditional type может дать сбой — понадобится доплогика. Также метод не работает с динамическими selection set.Типичные ошибки
* Забыть указать
as const — без него TypeScript не зафиксирует структуру запроса.* Использовать при сложных интерфейсах: conditional type не всегда корректно обрабатывает discriminated unions — это потребует дополнительных проверок.
* Не синхронизировать маппер: если схема меняется, типы рассинхронизируются, и вы получите ложные гарантии.
Практический совет
Применяйте этот подход для микросервисов с простой, стабильной схемой или когда codegen конфликтует с Relay-фрагментарной структурой. В production я так типизировал запросы в проекте, где схема менялась раз в неделю, а codegen падал из-за кэширования — это сэкономило время без потери безопасности типов.
Вывод: Generics и conditional types — это легковесная альтернатива codegen для типизации вложенных GraphQL-ответов, но она требует ручного управления маппером и подходит для схем ограниченного размера.
❤4👀2
Object.assign() и structuredClone() в React стейте: баги с прототипами и Date
При immutable-обновлении вложенных объектов в React стейте разработчики часто полагаются на поверхностное копирование через spread или Object.assign(), забывая, что эти методы не сохраняют прототипы и сериализуют Date в строку. Это приводит к тихим багам, которые проявляются только в production при работе с классами или сложными типами.
Баг с прототипами: Object.assign() обнуляет методы класса
Когда объект содержит экземпляр класса, Object.assign({}, obj) копирует только собственные свойства, игнорируя прототип. Например, методы полностью теряются:
Баг с Date: structuredClone() превращает дату в строку
В React стейте это особенно опасно для UI-компонентов, которые полагаются на методы Date (например, календари или таймеры). Типичная ошибка: разработчики думают, что structuredClone() копирует Date нативно, но по спецификации оно заменяет Date на строку UTC. Совет: используйте
Trade-off: поверхностное vs глубокое копирование
- Поверхностное копирование (spread, Object.assign) быстрое и простое, но не подходит для вложенных структур с прототипами или ссылочными типами (Date, Map). Риск: мутация общего стора через ссылки.
- Глубокое копирование (structuredClone, Immer) безопаснее, но медленнее и несовместимо с некоторыми типами. Практический совет: для production используйте Immer, который корректно работает с прототипами и Date, а structuredClone применяйте только для изолированных данных (cookies, clipboard).
Вывод: immutable-обновление в React требует осознанного выбора метода копирования в зависимости от типов данных в стейте, а не слепого использования spread "на всякий случай".
При immutable-обновлении вложенных объектов в React стейте разработчики часто полагаются на поверхностное копирование через spread или Object.assign(), забывая, что эти методы не сохраняют прототипы и сериализуют Date в строку. Это приводит к тихим багам, которые проявляются только в production при работе с классами или сложными типами.
Баг с прототипами: Object.assign() обнуляет методы класса
Когда объект содержит экземпляр класса, Object.assign({}, obj) копирует только собственные свойства, игнорируя прототип. Например, методы полностью теряются:
class User {
constructor(name) { this.name = name; }
greet() { return Hi, ${this.name}; }
}
const original = new User('Alice');
const copy = Object.assign({}, original);
console.log(copy.greet()); // TypeError: copy.greet is not a functionstructuredClone() решает эту проблему для сериализуемых типов, но классы с методами не поддерживаются — код упадет с DOMException. Решение: если в стейте лежат инстансы классов, используйте кастомную функцию глубокого копирования с проверкой прототипа через Object.getPrototypeOf.Баг с Date: structuredClone() превращает дату в строку
structuredClone() сериализует Date в строку, как JSON.stringify. После клонирования методы Date (getTime, toISOString) перестают работать:const state = { date: new Date('2024-12-25') };
const clone = structuredClone(state);
console.log(clone.date.getTime()); // TypeError: clone.date.getTime is not a functionВ React стейте это особенно опасно для UI-компонентов, которые полагаются на методы Date (например, календари или таймеры). Типичная ошибка: разработчики думают, что structuredClone() копирует Date нативно, но по спецификации оно заменяет Date на строку UTC. Совет: используйте
structuredClone только для данных, которые проходят полный список сериализуемых типов (Map, Set, ArrayBuffer), а для Date заводите отдельный хелпер или библиотеку вроде Immer.Trade-off: поверхностное vs глубокое копирование
- Поверхностное копирование (spread, Object.assign) быстрое и простое, но не подходит для вложенных структур с прототипами или ссылочными типами (Date, Map). Риск: мутация общего стора через ссылки.
- Глубокое копирование (structuredClone, Immer) безопаснее, но медленнее и несовместимо с некоторыми типами. Практический совет: для production используйте Immer, который корректно работает с прототипами и Date, а structuredClone применяйте только для изолированных данных (cookies, clipboard).
Вывод: immutable-обновление в React требует осознанного выбора метода копирования в зависимости от типов данных в стейте, а не слепого использования spread "на всякий случай".
👍5❤2🐳2👀1
Мемоизация в React 19: когда useMemo и memo уже не панацея, а новые use() и React Forget меняют правила игры
Сколько раз вы писали
Почему старые инструменты не идеальны
Мемоизация не бесплатна.
Новый хук use() — отказ от явного кеширования промисов
Хук
React Forget — компилятор как замена ручной оптимизации
React Forget анализирует граф вызовов и сам встраивает кеширование. Если функция вызывается с теми же аргументами, компилятор возвращает прошлое значение, причём без утечек памяти. Разработчику не надо думать, где ставить
Типичная ошибка: мемоизация как архитектурный костыль
Если ваш код на 80% состоит из
Вывод: Будущее за компиляторами, которые избавляют от рутины — useMemo и memo остаются инструментами для узких горячих путей, а не ежедневной практикой.
Сколько раз вы писали
useMemo(() => expensiveComputation(a, b), [a, b]) и сомневались, нужно ли это вообще? Или оборачивали компонент в memo, надеясь, что поверхностное сравнение пропсов не окажется дороже самого рендера? В React 19 старые подходы перестают быть единственно верными: появились инструменты, которые делают ручную мемоизацию узким, а не основным решением.Почему старые инструменты не идеальны
Мемоизация не бесплатна.
useMemo хранит ссылки и пересчитывает зависимости при каждом рендере. memo добавляет сравнение пропсов, и если дерево сложное, а данные меняются часто, выигрыша нет. В production я видел проекты, где 80% вызовов useMemo и useCallback не давали прироста — это была оптимизация ради оптимизации без профилирования. Типичная ошибка: мемоизировать все подряд, а не только горячие пути.Новый хук use() — отказ от явного кеширования промисов
Хук
use() компилируется в синхронный рендер и убирает необходимость вручную кешировать асинхронные данные. Пример: вместо того чтобы писать useEffect с загрузкой и оборачивать результат в useMemo, вы просто используете use(fetchUsers()). Компилятор сам решает, когда перерендерить компонент, без лишних проверок и ссылочных сравнений. Это особенно полезно для SSR и Suspense — мемоизация тут не нужна, рендер планируется автоматически.React Forget — компилятор как замена ручной оптимизации
React Forget анализирует граф вызовов и сам встраивает кеширование. Если функция вызывается с теми же аргументами, компилятор возвращает прошлое значение, причём без утечек памяти. Разработчику не надо думать, где ставить
useMemo — это происходит на уровне анализа зависимостей. Практический совет: используйте это на побочных проектах уже сейчас, но не спешите переписывать всё в проде. Для 95% сценариев React Forget достаточно, но на очень горячих путях — например, рендер таблицы на 10 тысяч строк — ручной useMemo всё ещё может быть оправдан.Типичная ошибка: мемоизация как архитектурный костыль
Если ваш код на 80% состоит из
useMemo и useCallback, это повод пересмотреть архитектуру, а не добавлять новые обёртки. use() берёт на себя загрузку данных, Suspense — ленивую подгрузку, а React Forget — всё остальное. Главный trade-off: экономия времени на ручной мемоизации против возможных дополнительных рендеров на этапе доработки компилятора. В production это редко становится проблемой, если дерево компонентов спроектировано с учётом React Forget.Вывод: Будущее за компиляторами, которые избавляют от рутины — useMemo и memo остаются инструментами для узких горячих путей, а не ежедневной практикой.
👍5👎2
Внутри статьи она подробно расписывает этапы собеседований, лайфхаки и делится учебными ресурсами, которые ей помогли.
Плюс девушка великодушно оставила ссылки на свой Notion с полезными заметками по математике и LLM.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤4👎4👀3
Реактивный DataV: оптимизация 10k+ точек с event-driven аннотациями и hit-тестингом
Когда на графике 10 000+ точек, часто выбирают: производительность или интерактивность. Прямой рендер в SVG тормозит, Canvas летает, но теряет hover и click. Это ложный компромисс.
Гибридный подход
Решение — Canvas для рендера точек (никаких DOM-узлов, только пиксели). Hover и click выносятся в event-driven аннотации через raycasting. Считай только при движении мыши, а не на каждый фрейм.
Ленивые аннотации и кеширование
Рендери только те аннотации, которые попадают в viewport, и кешируй результат. Иначе каждый чих дергает рендер на 10k точках.
Пример hit-тестинга
Без фреймворков, на TypeScript:
Сначала проверка по bounding box, потом по расстоянию. Для 10k точек O(n) срабатывает за пару миллисекунд. QuadTree даёт O(log n), но не всегда нужен.
Предупреждение о типичной ошибке
Главный подвох: не забудь сбросить аннотацию при уходе мыши с точки. Иначе график зависнет с подписями на все 10k, и пользователь решит, что интерфейс сломался.
Вывод:
Event-driven аннотации с hit-тестингом — это trade-off между точностью интерактивности и производительностью рендера, где Canvas даёт скорость, а raycasting сохраняет UX.
Когда на графике 10 000+ точек, часто выбирают: производительность или интерактивность. Прямой рендер в SVG тормозит, Canvas летает, но теряет hover и click. Это ложный компромисс.
Гибридный подход
Решение — Canvas для рендера точек (никаких DOM-узлов, только пиксели). Hover и click выносятся в event-driven аннотации через raycasting. Считай только при движении мыши, а не на каждый фрейм.
Ленивые аннотации и кеширование
Рендери только те аннотации, которые попадают в viewport, и кешируй результат. Иначе каждый чих дергает рендер на 10k точках.
Пример hit-тестинга
Без фреймворков, на TypeScript:
class ScatterPlot {
handleMouseMove(e) {
for (const p of this.points) {
if (Math.abs(mouseX - p.x) <= p.r * 2 &&
Math.abs(mouseY - p.y) <= p.r * 2) {
if (Math.hypot(mouseX - p.x, mouseY - p.y) <= p.r) {
this.showAnnotation(i);
return;
}
}
}
}
showAnnotation(index) {
requestAnimationFrame(() => this.renderAnnotationLayer());
}
}Сначала проверка по bounding box, потом по расстоянию. Для 10k точек O(n) срабатывает за пару миллисекунд. QuadTree даёт O(log n), но не всегда нужен.
Предупреждение о типичной ошибке
Главный подвох: не забудь сбросить аннотацию при уходе мыши с точки. Иначе график зависнет с подписями на все 10k, и пользователь решит, что интерфейс сломался.
Вывод:
Event-driven аннотации с hit-тестингом — это trade-off между точностью интерактивности и производительностью рендера, где Canvas даёт скорость, а raycasting сохраняет UX.
❤4
Типизация WebSocket событий: как discriminated unions + brand checks делают стейт-машину предсказуемой и спасают от багов с payload
Каждый, кто писал real-time фичи на React или Vue, сталкивался: диспатчишь событие WebSocket, а payload — не того типа. Структура похожа, семантика разная, и TypeScript молчит. Ошибка уходит в production.
Discriminated unions — база, но не панацея
Стандартный подход: поле
Brand checks — второй рубеж
Добавьте фиктивное поле
Пример типизации:
Типичная ошибка
Думать, что brand checks — это всё. WebSocket может прислать что угодно. Типы не панацея. В production обязательно докидывайте валидацию схемы на каждое входящее сообщение через Zod или io-ts.
Практический совет
Используйте brand checks как инструмент для разработчика, чтобы не протащить payload не туда при сборке стейта. А рантайм-валидацию — как первую линию защиты от кривых данных с сервера. Вместе они дают надёжность и предсказуемость в real-time архитектуре.
Вывод: Discriminated unions сужают типы по значению, brand checks защищают от семантических багов, а рантайм-валидация — от любых неожиданностей с сервера: три уровня защиты для production-grade стейт-машины.
Каждый, кто писал real-time фичи на React или Vue, сталкивался: диспатчишь событие WebSocket, а payload — не того типа. Структура похожа, семантика разная, и TypeScript молчит. Ошибка уходит в production.
Discriminated unions — база, но не панацея
Стандартный подход: поле
type сужает остальные поля. Но если два события содержат sessionId с разным смыслом, TS не заметит подмены. На уровне типов всё ок, в рантайме — треш.Brand checks — второй рубеж
Добавьте фиктивное поле
__brand: 'open' в payload. В рантайме его нет, типы не гоняют лишние данные. Компилятор не даст смешать payload разных событий. Это имитация номинативной типизации в структурной системе TypeScript. Стейт-машина перестаёт принимать невесть что: переходы становятся предсказуемыми.Пример типизации:
type WSMessage =
| { type: 'OPEN'; payload: { sessionId: string } & { __brand: 'open' } }
| { type: 'MSG'; payload: { userId: number; text: string } & { __brand: 'msg' } }
| { type: 'ERROR'; payload: { error: string } & { __brand: 'err' } };
Типичная ошибка
Думать, что brand checks — это всё. WebSocket может прислать что угодно. Типы не панацея. В production обязательно докидывайте валидацию схемы на каждое входящее сообщение через Zod или io-ts.
Практический совет
Используйте brand checks как инструмент для разработчика, чтобы не протащить payload не туда при сборке стейта. А рантайм-валидацию — как первую линию защиты от кривых данных с сервера. Вместе они дают надёжность и предсказуемость в real-time архитектуре.
Вывод: Discriminated unions сужают типы по значению, brand checks защищают от семантических багов, а рантайм-валидация — от любых неожиданностей с сервера: три уровня защиты для production-grade стейт-машины.
👎9❤5🐳2
Почему useSyncExternalStore проигрывает Zustand и Jotai на композитных сторах и как селекторы убивают производительность
Пытался вчера прикрутить
Бенчмарк на 1000 подписчиков: примитивы vs вложенные объекты
На простом сторе (один селектор, один примитив) разница в микросекундах — Jotai ~0.15ms, Zustand ~0.18ms,
Zustand без
Jotai с производными атомами — та же петрушка.
Грабли с селекторами: деструктуризация убивает производительность
Ошибка, которую я вижу на каждом проекте — деструктуризация в теле селектора:
Каждый вызов селектора создаёт новый объект.
Правильный подход — писать селекторы на отдельные поля:
Тогда
Дополнительные меры: Zustand — используйте
Когда useSyncExternalStore — зло для бизнес-логики
Сейчас на проекте простая архитектура: простые состояния — Zustand, меньше кода. Композитные данные — Jotai с
Вывод: Микрооптимизация селекторов и отказ от композитных возвращаемых объектов — это не premature optimization, а разница между 1.2ms и 0.3ms ререндера при масштабировании на 1000+ подписчиков.
Пытался вчера прикрутить
useSyncExternalStore к композитному стору в React 19. Думал, что если это API из коробки, то и проблем с производительностью не будет. На деле — пришлось переписывать половину.Бенчмарк на 1000 подписчиков: примитивы vs вложенные объекты
На простом сторе (один селектор, один примитив) разница в микросекундах — Jotai ~0.15ms, Zustand ~0.18ms,
useSyncExternalStore ~0.22ms. Можно не париться. Как только в сторе появляется вложенный объект — начинается треш.Zustand без
shallow перерисовывает всех подписчиков при любом изменении. Даже если твой селектор брал только state.user, а кто-то обновил state.posts. Причина: Zustand при каждом вызове селектора сравнивает результат через Object.is с предыдущим. Если селектор возвращает новый объект — привет, ререндер.Jotai с производными атомами — та же петрушка.
atom(get => { return { user: get(userAtom), posts: get(postsAtom) } }) — создаёт новый объект на каждый вызов. Jotai также сравнивает через Object.is, так что ререндер гарантирован.Грабли с селекторами: деструктуризация убивает производительность
Ошибка, которую я вижу на каждом проекте — деструктуризация в теле селектора:
const { user, posts } = useStore(state => ({
user: state.user,
posts: state.posts
}))Каждый вызов селектора создаёт новый объект.
Object.is никогда не вернёт true.Правильный подход — писать селекторы на отдельные поля:
const user = useStore(state => state.user)
const posts = useStore(state => state.posts)
Тогда
Object.is вернёт true, если ссылка не изменилась.Дополнительные меры: Zustand — используйте
shallow для сравнения. Jotai — atomWithComparator. Экономия до 40% ререндеров.Когда useSyncExternalStore — зло для бизнес-логики
useSyncExternalStore вообще не про бизнес-логику. Его пилили под внешние источники: WebSocket, IndexedDB. Внутри React он не батчит подписки, так что при 1000 подписчиков и частых изменениях — 1.2ms на ререндер. Jotai с мемоизацией — 0.3ms. Zustand с shallow — 0.6ms. Разница в 4 раза — это уже не погрешность.Сейчас на проекте простая архитектура: простые состояния — Zustand, меньше кода. Композитные данные — Jotai с
atomFamily и splitAtom. useSyncExternalStore — только если прикручиваешь что-то внешнее.Вывод: Микрооптимизация селекторов и отказ от композитных возвращаемых объектов — это не premature optimization, а разница между 1.2ms и 0.3ms ререндера при масштабировании на 1000+ подписчиков.
👎5🐳3👀2😁1😱1
requestAnimationFrame и React-батчинг: почему дёргается drag-and-drop
Сделал перетаскивание элементов в production? На быстрых движениях начинаются дёргания и выпадающие кадры. Проблема знакомая:
В React 18+ батчинг работает в рамках одного события, но rAF срабатывает до мерджа обновлений. Результат: анимация читает старую позицию, React применяет новые значения с задержкой, элемент дёргается.
Решение с flushSync
Для чтения DOM-значений (позиция, размер) используй
Предупреждение
Не злоупотребляй
Альтернативы
- Замени
- Если хочешь не трогать React-состояние внутри кадра, храни позиции в
Вывод:
Сделал перетаскивание элементов в production? На быстрых движениях начинаются дёргания и выпадающие кадры. Проблема знакомая:
requestAnimationFrame вызывается до того, как React применяет батч из setState. Особенно если rAF висит на mousemove вне реактовских обработчиков.В React 18+ батчинг работает в рамках одного события, но rAF срабатывает до мерджа обновлений. Результат: анимация читает старую позицию, React применяет новые значения с задержкой, элемент дёргается.
Решение с flushSync
Для чтения DOM-значений (позиция, размер) используй
useLayoutEffect — выполняется до показа кадра. Внутри drag-логики добавь синхронный вызов:const handleMouseMove = (e: MouseEvent) => {
ReactDOM.flushSync(() => {
setPos((prev) => ({
x: prev.x + e.movementX,
y: prev.y + e.movementY
}));
});
requestAnimationFrame(updateStyles);
};flushSync заставляет React применить батч синхронно перед следующим кадром. Дёргания уходят.Предупреждение
Не злоупотребляй
flushSync — он ломает Concurrent Mode и режет производительность, если вызывать на каждом mousemove. Типичная ошибка: считать его универсальным решением без оценки trade-offs.Альтернативы
- Замени
mousemove/touchmove на pointer-events — они синхроннее в браузере.- Если хочешь не трогать React-состояние внутри кадра, храни позиции в
useRef и обновляй DOM руками через rAF. Но это редкий кейс, и он разрушает реактивность.Вывод:
requestAnimationFrame и React-батчинг конфликтуют по умолчанию — контролируй синхронизацию через flushSync или выноси анимации за пределы реактовского цикла, иначе даже гладкий код споткнётся на быстром перемещении.👍9👀2
Signal vs Observable: почему ваш граф зависимостей взрывается в production
Когда в корзине интернет-магазина total = price * qty, затем скидка от суммы, затем налог — Observable (RxJS) на каждое изменение price пересчитывает все подписки, даже если данные не изменились. Разработчики навешивают distinctUntilChanged на каждую цепочку, и через месяц граф превращается в лапшу.
Как работают Signal
Signal (SolidJS, Vue 3.4+, Preact Signals, Svelte 5) делает вычисления ленивыми. Он отслеживает, какие сигналы реально читаются в computed, а не подписывается на всё подряд. Два сигнала, зависящих от одного источника, не подписываются дважды. При изменении источника пересчитываются только те, кто на него ссылается, а не всё дерево.
Пример на псевдокоде Solid:
При изменении count пересчитываются total и final. Discount пересчитывается только если total пересечет порог. В RxJS через combineLatest discount будет пересчитываться на каждое изменение count и price, даже если total не изменился. Меньше кода — больше бесполезной работы.
Когда Observable все еще нужен
Observable (RxJS) хорош для асинхронных цепочек: debounce, switchMap, WebSocket. Это декларативные потоки, где важны трансформации во времени. Но при сотне зависимостей начинается цирк: каждый чих триггерит пересчёт, дебажить dependency hell — отдельный квест. Типичная ошибка — использовать Observable для синхронного графа зависимостей, где сигналы дают ровно то же самое без лишних пересчётов.
Практический совет
Если у вас в проекте уже RxJS — не переписывайте всё. Для нового фича с interdependent состояниями (формы, редакторы, дашборды) закладывайтесь на сигналы. В продакшене с сотнями зависимостей сигналы дают на 30-50% меньше бесполезных перерисовок и упрощают отладку: граф зависимостей прозрачен, а не взрывается на каждой итерации.
Вывод: Signal — для синхронных графов с предсказуемыми изменениями, Observable — для event-driven асинхронных потоков, и выбор между ними — это trade-off между производительностью вычислений и гибкостью трансформаций.
Когда в корзине интернет-магазина total = price * qty, затем скидка от суммы, затем налог — Observable (RxJS) на каждое изменение price пересчитывает все подписки, даже если данные не изменились. Разработчики навешивают distinctUntilChanged на каждую цепочку, и через месяц граф превращается в лапшу.
Как работают Signal
Signal (SolidJS, Vue 3.4+, Preact Signals, Svelte 5) делает вычисления ленивыми. Он отслеживает, какие сигналы реально читаются в computed, а не подписывается на всё подряд. Два сигнала, зависящих от одного источника, не подписываются дважды. При изменении источника пересчитываются только те, кто на него ссылается, а не всё дерево.
Пример на псевдокоде Solid:
count = signal(1)
price = signal(100)
total = computed(() => count() * price())
discount = computed(() => total() > 500 ? 0.1 : 0)
final = computed(() => total() * (1 - discount()))
При изменении count пересчитываются total и final. Discount пересчитывается только если total пересечет порог. В RxJS через combineLatest discount будет пересчитываться на каждое изменение count и price, даже если total не изменился. Меньше кода — больше бесполезной работы.
Когда Observable все еще нужен
Observable (RxJS) хорош для асинхронных цепочек: debounce, switchMap, WebSocket. Это декларативные потоки, где важны трансформации во времени. Но при сотне зависимостей начинается цирк: каждый чих триггерит пересчёт, дебажить dependency hell — отдельный квест. Типичная ошибка — использовать Observable для синхронного графа зависимостей, где сигналы дают ровно то же самое без лишних пересчётов.
Практический совет
Если у вас в проекте уже RxJS — не переписывайте всё. Для нового фича с interdependent состояниями (формы, редакторы, дашборды) закладывайтесь на сигналы. В продакшене с сотнями зависимостей сигналы дают на 30-50% меньше бесполезных перерисовок и упрощают отладку: граф зависимостей прозрачен, а не взрывается на каждой итерации.
Вывод: Signal — для синхронных графов с предсказуемыми изменениями, Observable — для event-driven асинхронных потоков, и выбор между ними — это trade-off между производительностью вычислений и гибкостью трансформаций.
❤6🐳3👀3👍2😁1😱1
Как вывести типы контекстов во вложенных layout без ручных интерфейсов в React Router v7
Если вы используете TanStack Router (React Router v7) с вложенными layout, то наверняка сталкивались с ситуацией, когда родительский layout прокидывает loader-контекст, а дочерний компонент вынужден вручную объявлять интерфейс и кастить. Типовая ошибка — забыть обновить интерфейс при изменении структуры данных, что ведет к багам на проде.
Как работает типовая цепочка
Система типов строится вокруг дженериков createRoute. Каждый роут наследует контекст от родителя через getParentRoute(). TypeScript автоматически мержит результаты loader родительского и текущего роута:
Ни одного явного интерфейса или каста. Система выводит тип контекста из цепочки родительских роутов.
Production-кейс: аутентификация с правами
Практический пример: вложенный layout с авторизацией. Родитель кладет user, дочерний — permissions:
Страница получает { user, permissions } без ручного объявления. Если переименовать user в currentUser — TypeScript подсветит все места, где используется старый ключ.
Типичная ошибка и trade-off
Ошибка: ручное объявление interface IContext = { user: string } и последующий as any при несоответствии. Это ломает всю type-safety и ведет к runtime-ошибкам при изменении данных.
Trade-off: полагаться на вывод типов чуть сложнее читать в IDE, чем явные интерфейсы, но выигрыш в надежности при рефакторинге — код сам документирует контракты, и компилятор ловит несоответствия.
Вывод: Используйте автоматический вывод типов контекстов через цепочку getParentRoute, чтобы избавиться от ручных интерфейсов и кастингов, повысив надежность и упростив рефакторинг в production-коде.
Если вы используете TanStack Router (React Router v7) с вложенными layout, то наверняка сталкивались с ситуацией, когда родительский layout прокидывает loader-контекст, а дочерний компонент вынужден вручную объявлять интерфейс и кастить. Типовая ошибка — забыть обновить интерфейс при изменении структуры данных, что ведет к багам на проде.
Как работает типовая цепочка
Система типов строится вокруг дженериков createRoute. Каждый роут наследует контекст от родителя через getParentRoute(). TypeScript автоматически мержит результаты loader родительского и текущего роута:
const rootRoute = createRootRoute()({
loader: () => ({ user: 'John' })
});
const layoutRoute = createRoute({
getParentRoute: () => rootRoute,
loader: ({ context }) => ({
theme: context.user === 'John' ? 'dark' : 'light'
})
});
const pageRoute = createRoute({
getParentRoute: () => layoutRoute,
loader: ({ context }) => {
// context: { user: string, theme: string }
return context.theme;
}
});Ни одного явного интерфейса или каста. Система выводит тип контекста из цепочки родительских роутов.
Production-кейс: аутентификация с правами
Практический пример: вложенный layout с авторизацией. Родитель кладет user, дочерний — permissions:
const authLayout = createRoute({
getParentRoute: () => rootRoute,
loader: () => ({ user: fetchUser() })
});
const adminLayout = createRoute({
getParentRoute: () => authLayout,
loader: ({ context }) => ({
permissions: fetchPermissions(context.user.id)
})
});Страница получает { user, permissions } без ручного объявления. Если переименовать user в currentUser — TypeScript подсветит все места, где используется старый ключ.
Типичная ошибка и trade-off
Ошибка: ручное объявление interface IContext = { user: string } и последующий as any при несоответствии. Это ломает всю type-safety и ведет к runtime-ошибкам при изменении данных.
Trade-off: полагаться на вывод типов чуть сложнее читать в IDE, чем явные интерфейсы, но выигрыш в надежности при рефакторинге — код сам документирует контракты, и компилятор ловит несоответствия.
Вывод: Используйте автоматический вывод типов контекстов через цепочку getParentRoute, чтобы избавиться от ручных интерфейсов и кастингов, повысив надежность и упростив рефакторинг в production-коде.
👀4🐳2😱1
Типизация ErrorBoundary в React 19: асинхронные ошибки, error.cause и production-логирование
ErrorBoundary долго был слабым звеном — он не ловил ошибки из промисов, useEffect или setTimeout. React 19 не меняет классовый подход, но появление
Почему any в state — ошибка
Состояние границы должно быть строго типизировано:
Production-кейс с fetch
При HTTP 500 выбрасывай:
В границе проверяй:
Это позволяет не слать в Sentry временные сетевые сбои.
Типизация cause и instanceof
Используй
Вывод:
Типизированный ErrorBoundary с
ErrorBoundary долго был слабым звеном — он не ловил ошибки из промисов, useEffect или setTimeout. React 19 не меняет классовый подход, но появление
error.cause и правильная типизация через instanceof делают обработку асинхронных ошибок предсказуемой и безопасной.Почему any в state — ошибка
Состояние границы должно быть строго типизировано:
Error | null. В getDerivedStateFromError проверяй cause через instanceof Error, не через truthy check. Это исключает мусор из продакшена, где cause может быть строкой, числом или undefined.Production-кейс с fetch
При HTTP 500 выбрасывай:
throw new Error('HTTP error', { cause: res.status })В границе проверяй:
if (error.cause instanceof Error) {
// Логируем полный стек
} else if (typeof error.cause === 'number') {
// Игнорируем, если статус < 500
}Это позволяет не слать в Sentry временные сетевые сбои.
Типизация cause и instanceof
Используй
instanceof для разграничения классов ошибок — HTTPError, NetworkError, ValidationError. Так ты контролируешь, что идёт в Sentry, а что — только в консоль. Без этого логгер захлебнётся шумом.Вывод:
Типизированный ErrorBoundary с
error.cause и instanceof превращает асинхронные ошибки из невидимых багов в управляемые инциденты.👍4😱2🐳2❤1👎1
React 19: ErrorBoundary с типизацией, error.cause и строгое логирование
Границы ошибок в React долго выглядели как костыль для асинхронщины: раньше
Интерфейс и цепочка ошибок
Интерфейс
*
*
Production: сужение и безопасность
Передавать полный
Схема логирования через
* если
* если просто
* если вообще не
Типичная ошибка
Вывод: Типизированный
Источники: React docs про Error Boundaries, MDN про Error.cause, React 19 release notes про use() hook и async errors.
Границы ошибок в React долго выглядели как костыль для асинхронщины: раньше
ErrorBoundary отлавливал только синхронные ошибки, а запросы с then/catch просто пролетали мимо. В 19 версии это починили через use и новые хуки, но типизация все равно остается местом, где можно наступить на грабли в production.Интерфейс и цепочка ошибок
Интерфейс
ErrorBoundaryState с Error | null, getDerivedStateFromError ловит ошибку, componentDidCatch отправляет в лог. Но что действительно спасает в реальных кейсах — это error.cause из ES2022. Когда fetch падает с 401, ты пробрасываешь не строку, а объект:*
error.cause?.status для проверки статуса*
error.cause?.message для текстаinterface AppError extends Error {
cause?: {
status: number;
message: string;
};
}Production: сужение и безопасность
Передавать полный
stack клиенту — плохая идея. Там пути файлов и внутренние адреса. Мы обрезаем первые 200 символов, если ошибка некастомная, и никогда не светим причину наружу: error.cause только во внутренний логгер.Схема логирования через
instanceof:* если
error instanceof AppError — пишем структурированно с типом, причиной и стеком* если просто
Error — обрезаем стек, кидаем предупреждение* если вообще не
Error — логируем как строку и показываем generic fallbackТипичная ошибка
instanceof не работает, если ошибка прилетела из другого iframe или realm. Там error.cause может быть сериализованным объектом, и придется проверять по полям вручную. Встречал такое при интеграции с микросервисными виджетами. Еще один момент — не засовывай всю логику в componentDidCatch: вынеси ее в отдельную функцию handleFatalError с возвратом never и переиспользуй, иначе при рефакторинге придется переписывать каждый Boundary.Вывод: Типизированный
ErrorBoundary с error.cause и строгим логированием через instanceof — это надежный паттерн для обработки асинхронных ошибок, который дает консистентность и безопасность в production.Источники: React docs про Error Boundaries, MDN про Error.cause, React 19 release notes про use() hook и async errors.
👍6🐳2👀2😱1
Два способа создания доступного DatePicker'а с помощью AI: 80/20 или системное проектирование
DatePicker потребовался на React и TypeScript с корректной работой keyboard navigation, screen reader'а, управляемым состоянием и проверками доступности. Первый способ — дать AI четкий запрос, получить 80% кода, остальное доработать руками. Модель генерирует структуру календаря, атрибуты ARIA, базовую keyboard navigation и логику работы с датами. Затем начинаются проблемы: поведение фокуса становится нестабильным, возникают конфликты обработчиков событий, озвучивание screen reader'ами требует тестирования, небольшое изменение в логике может нарушить календарь.
Второй способ — системное проектирование с AI-агентом. Включает PRD, декомпозицию задач, правила агента, внешнюю верификацию, Vitest, Playwright, сборку Vite, проверки типов и строгий цикл: агент не может двигаться дальше, пока не пройден текущий шаг. Далее разбирается, в чем AI действительно помог, где начал сбиваться, почему одного большого запроса недостаточно, как The Verifier изменил процесс и почему задача инженера сводится к контролю над замыслом, архитектурой, контрактами и стоимостью изменений.
Читать далее на Habr
DatePicker потребовался на React и TypeScript с корректной работой keyboard navigation, screen reader'а, управляемым состоянием и проверками доступности. Первый способ — дать AI четкий запрос, получить 80% кода, остальное доработать руками. Модель генерирует структуру календаря, атрибуты ARIA, базовую keyboard navigation и логику работы с датами. Затем начинаются проблемы: поведение фокуса становится нестабильным, возникают конфликты обработчиков событий, озвучивание screen reader'ами требует тестирования, небольшое изменение в логике может нарушить календарь.
Второй способ — системное проектирование с AI-агентом. Включает PRD, декомпозицию задач, правила агента, внешнюю верификацию, Vitest, Playwright, сборку Vite, проверки типов и строгий цикл: агент не может двигаться дальше, пока не пройден текущий шаг. Далее разбирается, в чем AI действительно помог, где начал сбиваться, почему одного большого запроса недостаточно, как The Verifier изменил процесс и почему задача инженера сводится к контролю над замыслом, архитектурой, контрактами и стоимостью изменений.
Читать далее на Habr
⚡3😁2🐳2😱1👀1
Typed custom hooks: constraints и infer для строгих конфигов
Часто вижу, как
Constraints: фиксируем рамки
Используй
Infer: выводим сложные зависимости
Когда конфиг содержит вложенные коллбэки или динамические ключи,
Типичная ошибка: забытый as const
Без
Вывод: Constraints с
Часто вижу, как
custom hooks принимают конфиги, где TypeScript выводит mode: string вместо 'edit' | 'view', а permissions — string[] вместо конкретных литералов. В production это ведет к потере автодополнения и багам на стороне потребителей.Constraints: фиксируем рамки
Используй
extends уже на уровне дженериков хука, чтобы сузить допустимые типы. Без него TS выводит широкие типы, с ним — точные литералы, если исходные данные переданы с as const.function useFeature<M extends string, P extends string[]>(config: {
mode: M;
permissions: P;
}) {
// TS: M, P как литералы
}Infer: выводим сложные зависимости
Когда конфиг содержит вложенные коллбэки или динамические ключи,
infer в условных типах автоматически извлекает нужные типы из переданного объекта. Это спасает от ручного аннотирования.type ConfigResult<T> = T extends { mode: infer M; permissions: infer P }
? { mode: M; permissions: P }
: never;Типичная ошибка: забытый as const
Без
as const TS выведет string, а не литералы. А если конфиг приходит из API — этот подход бесполезен. Зато для статически заданных конфигов в хуках (фичи, A/B-тесты, permission guards) это чистый профит: типы не протекают, код самодокументируется.Вывод: Constraints с
extends и вывод через infer дают строгую типизацию factory-функций и конфигурационных объектов без потери читаемости и с проверками на уровне компиляции.👍4❤3