Skip to content

React-хуки ​

@macrulez/inview-react даёт те же основные возможности, что и Vue-composables, построенные на useSyncExternalStore поверх тех же framework-agnostic движков ядра — тот же API, то же поведение, зеркалированное 1:1 — плюс собственный render-prop компонент <InView> и <InviewProvider> для значений по умолчанию на уровне приложения — React-эквиваленты Vue-шных v-reveal/<InView> и viewportDefaults/setViewportDefaults(). Импортируйте всё из @macrulez/inview-react.

Отличия от Vue — структурные, а не поведенческие: обычные значения вместо Ref (без .value), простой HTMLElement | null для target вместо MaybeRefOrGetter (у React нет примитива «реф как реактивное значение», который можно было бы туда принять — используйте callback-реф с состоянием, а не обычный useRef, если target должен меняться после монтирования), и статичный снапшот, который возвращается на сервере через getServerSnapshot. Что именно значит каждое поле — смотрите на связанных страницах ядра/Vue ниже, здесь перечислены только React-специфичные моменты.

@macrulez/inview-react также реэкспортирует всю поверхность @macrulez/inview-core, включая примитивы с собственными отдельными хуками ниже (useReveal, useRevealController, usePrefersReducedMotion, useStagger, useScrollTo, useScrollSpy).

Реактивное состояние скролла ​

useScroll() — React-эквивалент Реактивного состояния скролла.

ts
function useScroll(
  target?: Window | HTMLElement | null,
  options?: UseScrollOptions,
): UseScrollReturn

Те же поля (x, y, direction, progress, velocity, isScrolling, scrollTo), но как обычные значения вместо рефов. options.idleTimeout (по умолчанию 150) работает так же.

Возвращаемое значение ​

ПолеТип
x, ynumber
direction'up' | 'down' | 'left' | 'right' | null
progress, velocitynumber
isScrollingboolean
scrollTo(position: number | { x?: number; y?: number }, options?: ScrollToOptions) => void

Пример:

tsx
import { useScroll } from '@macrulez/inview-react'

function ScrollIndicator() {
  const { y, isScrolling } = useScroll()
  return isScrolling ? <p>{y}px</p> : null
}

Видимость элемента ​

useElementVisibility() — React-эквивалент Видимости элемента.

ts
function useElementVisibility(
  target: HTMLElement | null,
  options?: UseElementVisibilityOptions,
): UseElementVisibilityReturn

Использует тот же общий пулящийся движок видимости. options — threshold, rootMargin, once, onEnter, onLeave — той же формы, что и во Vue, кроме root: HTMLElement | null (обычное значение, не MaybeRefOrGetter). Изменение target/threshold/ rootMargin/root переподписывается по-настоящему (членство в пуле привязано к ним); once, onEnter и onLeave вместо этого обновляют уже существующую подписку — каждый рендер передаёт свежие замыкания, так что onEnter/onLeave никогда не устаревают, в отличие от наивной версии на useEffect([]). Свежий литерал массива [0, 0.5] для threshold на каждый рендер тоже не вызывает переподписку — он сравнивается по значению, а не по ссылке.

threshold/rootMargin/once берут значение по умолчанию из <InviewProvider>, когда хук рендерится внутри него, и 0/'0px'/false иначе — React-эквивалент Vue-шного viewportDefaults, только читается из контекста, а не из модульного объекта. Опция, переданная напрямую в хук, всегда побеждает провайдер.

Возвращаемое значение ​

ПолеТип
isVisibleboolean
rationumber

Reveal-переключатель (useReveal) ​

useReveal() — React-эквивалент v-reveal: оборачивает useElementVisibility и дополнительно переключает класс (по умолчанию 'in') и/или data-атрибут на target по мере попадания во viewport — для обычного случая «просто переключить класс» без classList.toggle руками в эффекте. Для чего-то более кастомного, чем переключение класса/атрибута, используйте useElementVisibility напрямую.

ts
function useReveal(target: HTMLElement | null, options?: UseRevealOptions): UseRevealReturn

Опции ​

ПолеТипПо умолчанию
activeClassstring | null'in' — null отключает
activeAttributestring | nullnull
oncebooleanfalse
thresholdnumber | number[]—
rootMarginstring—
rootHTMLElement | null—
onEnter / onLeave(info: IntersectionInfo) => void—

Возвращаемое значение ​

Та же форма, что у Видимости элемента выше — isVisible, ratio.

tsx
import { useState } from 'react'
import { useReveal } from '@macrulez/inview-react'

function Card() {
  const [el, setEl] = useState<HTMLElement | null>(null)
  useReveal(el, { once: true })
  return (
    <div ref={setEl} className="reveal">
      ...
    </div>
  )
}

Значения по умолчанию на уровне приложения (<InviewProvider>) ​

<InviewProvider> / useInviewDefaults() — значения по умолчанию threshold/rootMargin/once на уровне приложения, которые читают useElementVisibility() и <InView> — чтобы не повторять их на каждом месте вызова. React-эквивалент viewportDefaults/setViewportDefaults() из Vue-адаптера — контекст вместо общего модульного объекта.

tsx
import { InviewProvider } from '@macrulez/inview-react'

function App() {
  return (
    <InviewProvider defaults={{ threshold: 0.2, once: true }}>
      <Page />
    </InviewProvider>
  )
}

Любой вызов useElementVisibility(el, { threshold: 0.6 }) внутри <Page> всё равно получит threshold: 0.6 — опция, переданная напрямую в хук, всегда побеждает провайдер. useInviewDefaults() читает то же объединённое значение напрямую, для собственного компонента, которому нужны итоговые значения по умолчанию без прохода через useElementVisibility.

Пропсы ​

defaults ​

Partial<{ threshold: number | number[]; rootMargin: string; once: boolean }> · по умолчанию: {}

Накладывается поверх общих для пакета значений по умолчанию (threshold: 0, rootMargin: '0px', once: false) — переопределяются только переданные поля.

useInviewDefaults() ​

ts
function useInviewDefaults(): { threshold: number | number[]; rootMargin: string; once: boolean }

Возвращает текущие значения <InviewProvider>, либо значения по умолчанию для всего пакета, если вызван вне провайдера — так существующий код без провайдера в дереве продолжает вести себя точно так же, как раньше.

<InView> ​

Render-prop-обёртка вокруг useElementVisibility для случаев, когда измеряемый элемент — это как раз тот, чей рендер зависит от результата (например, лениво монтировать тяжёлый дочерний компонент) — здесь useState-based callback-реф был бы лишним бойлерплейтом вокруг того же самого. Форма отличается от Vue-шного <InView>: children здесь — функция (render prop), а не scoped-слот.

tsx
import { InView } from '@macrulez/inview-react'

;<InView once>{({ isVisible }) => (isVisible ? <Heavy /> : null)}</InView>

Пропсы ​

ПолеТипПо умолчанию
oncebooleanзначение по умолчанию из <InviewProvider>
thresholdnumber | number[]значение по умолчанию из <InviewProvider>
rootMarginstringзначение по умолчанию из <InviewProvider>
rootHTMLElement | nullnull
asимя тега (keyof JSX.IntrinsicElements)'div' — измеряемый оборачивающий элемент
onEnter / onLeave(info: IntersectionInfo) => void—
children(state: { isVisible: boolean; ratio: number }) => ReactNodeобязателен — render prop

Всегда рендерит один реальный оборачивающий элемент (as, по умолчанию 'div') — IntersectionObserver нужен настоящий элемент для измерения.

Позиция элемента во viewport ​

useElementViewport() — React-эквивалент Позиции элемента во viewport.

ts
function useElementViewport(
  target: HTMLElement | null,
  options?: UseElementViewportOptions,
): UseElementViewportReturn

Возвращает ту же форму — rect, viewportProgress, distanceFromCenter — как обычные значения. options.root (по умолчанию null, окно) измеряет viewportProgress/distanceFromCenter относительно скроллящегося контейнера вместо окна — rect при этом всегда остаётся привязанным к окну.

Параллакс-слой ​

useParallaxLayer() — React-эквивалент Параллакс-слоя.

ts
function useParallaxLayer(
  target: HTMLElement | null,
  options: UseParallaxLayerOptions,
): UseParallaxLayerReturn

Те же опции (speed обязателен, axis, clamp, easing, range, root), та же логика преобразования, то же поведение с prefers-reduced-motion. style/progress мемоизируются через useMemo, а не являются Vue-шным computed.

Возвращаемое значение ​

ПолеТип
style{ transform: string }
progressnumber

Пример:

tsx
import { useParallaxLayer } from '@macrulez/inview-react'

function ParallaxImage() {
  const ref = useRef<HTMLImageElement | null>(null)
  const { style } = useParallaxLayer(ref.current, { speed: 0.6 })
  return <img ref={ref} style={style} src="/hero.jpg" />
}

Контроллер reveal-эффектов (useRevealController) ​

useRevealController() — React-эквивалент useRevealController: обёртка жизненного цикла вокруг createRevealController, создающая контроллер в эффекте и уничтожающая его автоматически при размонтировании.

ts
function useRevealController(options?: RevealControllerOptions): RevealController
tsx
import { useRevealController } from '@macrulez/inview-react'

function App() {
  useRevealController({ stagger: { step: 70, max: 4 } })
  return <>{/* ...карточки с className="reveal", включая добавленные асинхронной подгрузкой */}</>
}

options захватываются один раз, при первом рендере, и используются на весь срок жизни контроллера — та же одноразовая модель опций, что и у самого createRevealController. Возвращаемые методы refresh/observe/unobserve/destroy проксируют к инстансу контроллера после того, как отработал эффект монтирования — вызывайте их из обработчика события или другого эффекта, а не из тела рендера.

Прямой вызов createRevealController в собственном эффекте по-прежнему работает точно так же для всего, что не покрывает обёртка:

tsx
import { useEffect } from 'react'
import { createRevealController } from '@macrulez/inview-react'

function App() {
  useEffect(() => {
    const controller = createRevealController({ stagger: { step: 70, max: 4 } })
    return () => controller.destroy()
  }, [])

  return <>{/* ... */}</>
}

Предпочтение сниженной анимации (usePrefersReducedMotion) ​

usePrefersReducedMotion() — реактивное prefers-reduced-motion: reduce, в отличие от одноразовой статичной проверки prefersReducedMotion(). Построен на useSyncExternalStore, подписывается на событие change медиа-запроса; на сервере возвращает false.

ts
function usePrefersReducedMotion(): boolean
tsx
import { usePrefersReducedMotion } from '@macrulez/inview-react'

function Animated() {
  const reduced = usePrefersReducedMotion()
  return <div className={reduced ? undefined : 'spin'} />
}

Задержка стаггера (useStagger) ​

useStagger() — тонкая обёртка вокруг staggerDelay() для паритета с Vue-адаптером; вычисление достаточно дешёвое (простая арифметика), чтобы просто вызывать его каждый рендер — useMemo не нужен.

ts
function useStagger(index: number, options?: StaggerDelayOptions): string
tsx
import { useStagger } from '@macrulez/inview-react'

function Item({ index }: { index: number }) {
  const delay = useStagger(index, { step: 70, max: 4 })
  return <div className="reveal" style={{ transitionDelay: delay }} />
}

Прокрутка к элементу (useScrollTo) ​

useScrollTo() — возвращает колбэк scrollTo(el, options?), плавно скроллящий target (по умолчанию window) так, чтобы el оказался у его края, со смещением в пикселях — случай «доскроллить до элемента со смещением», который собственный scrollTo у useScroll() не покрывает (тот принимает только сырую позицию x/y).

ts
function useScrollTo(
  target?: Window | HTMLElement | null,
): (el: Element | null | undefined, options?: UseScrollToOptions) => void

target захватывается один раз, в массиве зависимостей useCallback — передайте новый target, чтобы получить колбэк, привязанный к другому контейнеру. Возвращаемый колбэк — no-op, если el/target равны null.

tsx
import { useScrollTo } from '@macrulez/inview-react'

function JumpButton({ sectionEl }: { sectionEl: HTMLElement | null }) {
  const scrollTo = useScrollTo()
  return <button onClick={() => scrollTo(sectionEl, { offset: 80 })}>Перейти</button>
}

Scroll Spy (useScrollSpy) ​

useScrollSpy() — React-эквивалент Vue composable со страницы Scroll Spy: отслеживает, какой из targets сейчас находится в активной полосе, и возвращает его индекс (или null).

ts
function useScrollSpy(
  targets: Array<HTMLElement | null | undefined>,
  options?: UseScrollSpyOptions,
): number | null

targets должен быть стабильным массивом (например, из useState, а не свежий литерал [...] на каждый рендер) — новая ссылка на массив вызывает настоящую переподписку, так же как target/root в остальных местах пакета. options захватываются один раз, при изменении самого targets — та же одноразовая модель, что у useRevealController выше.

tsx
import { useState } from 'react'
import { useScrollSpy } from '@macrulez/inview-react'

function Nav() {
  const [sections] = useState<(HTMLElement | null)[]>(() => [])
  const activeIndex = useScrollSpy(sections)
  return (
    <nav>
      {items.map((item, i) => (
        <a key={item.id} className={i === activeIndex ? 'active' : undefined}>
          {item.title}
        </a>
      ))}
    </nav>
  )
}

Любой другой экспорт @macrulez/inview-core — createScrollEngine, createVisibilityEngine, createElementTracker, createRevealController, createScrollSpy, ObserverPool/observerPool, rafLoop, staggerDelay, mapRange, bindCSSVar, prefersReducedMotion, clamp, easings, scrollToElement — доступен из @macrulez/inview-react точно так же, отдельная зависимость @macrulez/inview-core не нужна.