Skip to content

Хук useReactiveValue

Хук для извлечения значения из Signal/Computed/Resource и авто-ререндера компонента. Поддерживает React 18+ и ленивые фабрики без утечек памяти. Предположим, что counterSignal и doubleComputed созданы где-то в вашем приложении. Хук сам подпишется, вытащит значение наружу и будет триггерить ререндер компонента.

Пример

tsx
import React from 'react';
import { useReactiveValue } from './useReactiveValue';
import { counterSignal, doubleComputed } from './myState';
// Предположим, что counterSignal и doubleComputed созданы где-то в вашем приложении

export const StandaloneCounter = () => {
  // Хук сам подпишется, вытащит значение наружу и будет триггерить ререндер компонента
  const count = useReactiveValue(counterSignal);
  const doubleCount = useReactiveValue(doubleComputed);

  return (
    <div>
      <h4>Компонент на базе изолированного useReactiveValue</h4>
      <p>Обычное значение: {count}</p>
      <p>Удвоенное значение: {doubleCount}</p>

      <button onClick={() => counterSignal.value++}>Увеличить</button>
    </div>
  );
};

Небольшое погружение в исходники

ts
import { useSyncExternalStore, useCallback, useMemo, useEffect, useRef } from 'react'
import { CleanupFn } from '../../../core'

interface ObservableItem<T> {
  readonly value: T;
  subscribe: (cb: (val: T) => void) => CleanupFn
  destroy?: () => void
}

type ReactiveInput<T> = ObservableItem<T> | (() => ObservableItem<T>)

/**
 * Нативный React-хук для извлечения текущего значения из реактивных примитивов ядра
 * (Signal, Computed, Resource) и автоматического управления подпиской на рендеринг.
 *
 * Опирается на каноничную шим-прослойку `useSyncExternalStore`, что гарантирует 100%
 * совместимость с механизмами батчинга обновлений React 18+ и Concurrent Mode (защита от Tearing).
 *
 * ### Особенности управления памятью:
 * 1. **Глобальные сигналы (Сервисы/Синглтоны):** При размонтировании компонента хук выполняет
 *    только стандартную отписку от обновлений. Метод `.destroy()` **не вызывается**,
 *    что сохраняет глобальное состояние системы в безопасности.
 * 2. **Локальные фабрики (`() => engine.computed(...)`):** Если в качестве аргумента передана
 *    функция-фабрика, хук понимает, что вычисление создано локально для этого экрана.
 *    При анмаунте компонента хук автоматически вызовет `.destroy()`, предотвращая утечки памяти в ядре.
 *
 * @template T - Тип данных, инкапсулированных внутри реактивного элемента.
 * @param {ReactiveInput<T>} input - Готовый реактивный элемент ядра или ленивая функция-фабрика, возвращающая его.
 * @returns {T} Актуальное синхронизированное значение реактивного элемента.
 *
 * @example
 * ```tsx
 * import { useReactiveValue } from '@pravosleva/reactive-engine/react';
 * import { userInfoService } from '~/store';
 *
 * // Сценарий 1: Прямая подписка на долгоживущий сигнал сервиса
 * export const CounterDisplay = () => {
 *   const counter = useReactiveValue(userInfoService.counter);
 *   return <span>Значение: {counter}</span>;
 * };
 *
 * // Сценарий 2: Использование ленивой фабрики для локальных вычислений (авто-очистка ядра при анмаунте)
 * export const FilteredList = ({ query }: { query: string }) => {
 *   const filteredData = useReactiveValue(() =>
 *     engine.computed(() => userInfoService.list.value.filter(item => item.includes(query)))
 *   );
 *   return <ul>{filteredData.map(item => <li key={item}>{item}</li>)}</ul>;
 * };
 * ```
 */
export const useReactiveValue = <T>(input: ReactiveInput<T>): T => {
  const isFactory = typeof input === 'function'

  // NOTE: СТАБИЛИЗАЦИЯ ФАБРИКИ: Сохраняем ссылку на функцию в ref,
  // чтобы не перезапускать вычисления, если разработчик передал инлайн-стрелочную функцию.
  const factoryRef = useRef(input)
  useEffect(() => {
    factoryRef.current = input
  }, [input])

  // Вычисляем элемент строго один раз при инициализации, либо при изменении стабильной ссылки на готовый сигнал
  const reactiveItem = useMemo(() => {
    if (typeof input === 'function') {
      // Вызываем фабрику только при первом проходе
      return input()
    }
    return input // Если передан готовый сигнал/ресурс — используем его напрямую
  }, [isFactory ? undefined : input]) // Стабильный массив зависимостей для фабрик!

  const subscribe = useCallback(
    (reactCallback: () => void) => {
      return reactiveItem.subscribe(reactCallback)
    },
    [reactiveItem]
  )

  const getSnapshot = useCallback(() => {
    return reactiveItem.value
  }, [reactiveItem])

  // NOTE: БЕЗОПАСНАЯ ОЧИСТКА ПАМЯТИ:
  // Вызываем .destroy() ТОЛЬКО если объект пришел из локальной фабрики `() => engine.computed(...)`.
  // Если это глобальный сигнал из сервиса — мы делаем только стандартный unmount подписки через useSyncExternalStore.
  useEffect(() => {
    return () => {
      if (isFactory && reactiveItem && typeof reactiveItem.destroy === 'function') {
        reactiveItem.destroy()
      }
    }
  }, [reactiveItem, isFactory])

  return useSyncExternalStore(subscribe, getSnapshot)
}