Типизация и детерминированное управление ресурсами через FinalizationRegistry и WeakRef...
Типизация и детерминированное управление ресурсами через FinalizationRegistry и WeakRef в долгоживущих хуках с Suspense React 19 с Suspense вводит состояние "засыпания" компонента, когда эффекты не запускаются и не очищаются. Это создает риск утечек WebSocket-соединений или Worker'ов, которые разработчики часто пытаются закрыть только через cleanup useEffect, не учитывая этот сценарий. Проблема: useEffect не срабатывает при suspend Когда компонент "засыпает", cleanup не вызывается, и ресурсы висят до размонтирования. Обычная практика с useRef удерживает ссылку, блокируя GC: const wsRef = useRef<WebSocket>(null); wsRef.current = new WebSocket('wss://...'); // cleanup не сработает при suspend return () => wsRef.current?.close(); Ошибка: useRef удерживает сильную ссылку, не позволяя сборщику мусора освободить WebSocket даже при отсутствии эффекта. Решение: типизированный менеджер ресурсов Используем WeakRef (слабая ссылка) + FinalizationRegistry (обратный вызов при GC): type ResourceCleanup = () => void; function createResourceManager<T extends object>( name: string, cleanup: (resource: T) => void ) { const registry = new FinalizationRegistry<ResourceCleanup>( (cleanupFn) => cleanupFn() ); return { register(resource: T) { const ref = new WeakRef(resource); registry.register(resource, () => { const res = ref.deref(); if (res) cleanup(res); }); return ref; }, unregister(resource: T) { registry.unregister(resource); } }; } Применение в хуке с Suspense: const wsManager = createResourceManager('chat-ws', (ws) => ws.close()); function ChatComponent() { const wsRef = useRef<WeakRef<WebSocket> | null>(null); useEffect(() => { const ws = new WebSocket('wss://chat'); wsRef.current = wsManager.register(ws); return () => wsManager.unregister(ws); }, []); } Смысл: WeakRef не блокирует GC, а FinalizationRegistry гарантирует cleanup после удаления объекта. Типизация через ManagedWeakRef<T>: type ManagedWeakRef<T> = { ref: WeakRef<T>; unregister: () => void }; Важное предупреждение FinalizationRegistry не гарантирует вызов — он приблизительный и может не сработать при интенсивной работе GC. Поэтому всегда комбинируй с явным unregister() в cleanup useEffect. Не злоупотребляй WeakRef в каждом хуке — это низкоуровневый инструмент для редких кейсов (долгоживущие стримы, кэши вне React). Вывод: WeakRef и FinalizationRegistry — последний рубеж защиты от утечек при Suspense, но детерминизм достигается только комбинацией с явной очисткой в cleanup, а не надеждой на GC.