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, useState } from 'react'

type CleanupFn = () => void

// Описываем строгий контракт для примитивов реактивного ядра (Signals / Computed / Resources)
export interface ISignalLike<V> {
  readonly value: V;
  subscribe: (cb: (val: V) => void) => CleanupFn
  destroy?: () => void
}

type ReactiveInput<T> = ISignalLike<T> | (() => ISignalLike<T>) | object | (() => object)

/**
 * Нативный React-хук для извлечения текущего значения из реактивных примитивов ядра
 * (Signal, Computed, Resource) и глубоких реактивных Proxy-объектов (reactive).
 *
 * @template T - Тип входящих данных
 * @param {ReactiveInput<any>} input - Готовый реактивный элемент ядра или функция-фабрика
 * @returns {any} Актуальное синхронизированное значение элемента или сам прокси-объект
 */
export const useReactiveValue = <T>(input: ReactiveInput<any>): any => {
  const isFactory = typeof input === 'function'

  // Стабилизируем ссылку на фабрику
  const factoryRef = useRef(input)
  useEffect(() => {
    factoryRef.current = input
  }, [input])

  // Вычисляем целевой элемент один раз
  const reactiveItem = useMemo(() => {
    if (typeof input === 'function') {
      return input()
    }
    return input
  }, [isFactory ? undefined : input])

  // Определяем, является ли объект примитивом (Signal/Computed) или это Proxy-объект reactive()
  const isSignal = reactiveItem &&
                   typeof reactiveItem === 'object' &&
                   'subscribe' in reactiveItem &&
                   typeof (reactiveItem as Record<string, unknown>).subscribe === 'function'

  // ------------------------------------------------====
  // СЦЕНАРИЙ А: Работа с атомарными Сигналами / Computed (useSyncExternalStore)
  // ------------------------------------------------====
  const subscribe = useCallback(
    (reactCallback: () => void) => {
      if (isSignal) {
        return (reactiveItem as ISignalLike<unknown>).subscribe(reactCallback)
      }
      return () => {}
    },
    [reactiveItem, isSignal]
  )

  const getSnapshot = useCallback(() => {
    if (isSignal) {
      return (reactiveItem as ISignalLike<unknown>).value
    }
    return reactiveItem
  }, [reactiveItem, isSignal])

  // Безопасная очистка локальных фабрик ядра при размонтировании экрана
  useEffect(() => {
    return () => {
      if (isFactory && reactiveItem && 'destroy' in reactiveItem && typeof (reactiveItem as any).destroy === 'function') {
        (reactiveItem as any).destroy()
      }
    }
  }, [reactiveItem, isFactory])

  // Если это сигнал — используем Concurrent-безопасныйuseSyncExternalStore
  if (isSignal) {
    return useSyncExternalStore(subscribe, getSnapshot)
  }

  // ------------------------------------------------====
  // СЦЕНАРИЙ Б: Работа с глубокими Proxy-объектами / Массивами
  // ------------------------------------------------====
  // Для Proxy-объектовuseSyncExternalStore не подходит, так как у них нет .value.
  // Мы создаем локальный forceUpdate и подписываем его через скрытый геттер __subscribe адаптера
  const [, forceUpdate] = useState([])

  useEffect(() => {
    if (reactiveItem && '__subscribe' in reactiveItem && typeof (reactiveItem as any).__subscribe === 'function') {
      const unsubscribe = (reactiveItem as any).__subscribe(() => {
        forceUpdate([]) // Провоцируем ререндер компонента через автобатчинг микрозадач
      })
      return () => unsubscribe()
    }
  }, [reactiveItem])

  return reactiveItem
}