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-эквивалент Реактивного состояния скролла.
function useScroll(
target?: Window | HTMLElement | null,
options?: UseScrollOptions,
): UseScrollReturnТе же поля (x, y, direction, progress, velocity, isScrolling, scrollTo), но как обычные значения вместо рефов. options.idleTimeout (по умолчанию 150) работает так же.
Возвращаемое значение
| Поле | Тип |
|---|---|
x, y | number |
direction | 'up' | 'down' | 'left' | 'right' | null |
progress, velocity | number |
isScrolling | boolean |
scrollTo | (position: number | { x?: number; y?: number }, options?: ScrollToOptions) => void |
Пример:
import { useScroll } from '@macrulez/inview-react'
function ScrollIndicator() {
const { y, isScrolling } = useScroll()
return isScrolling ? <p>{y}px</p> : null
}Видимость элемента
useElementVisibility() — React-эквивалент Видимости элемента.
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, только читается из контекста, а не из модульного объекта. Опция, переданная напрямую в хук, всегда побеждает провайдер.
Возвращаемое значение
| Поле | Тип |
|---|---|
isVisible | boolean |
ratio | number |
Reveal-переключатель (useReveal)
useReveal() — React-эквивалент v-reveal: оборачивает useElementVisibility и дополнительно переключает класс (по умолчанию 'in') и/или data-атрибут на target по мере попадания во viewport — для обычного случая «просто переключить класс» без classList.toggle руками в эффекте. Для чего-то более кастомного, чем переключение класса/атрибута, используйте useElementVisibility напрямую.
function useReveal(target: HTMLElement | null, options?: UseRevealOptions): UseRevealReturnОпции
| Поле | Тип | По умолчанию |
|---|---|---|
activeClass | string | null | 'in' — null отключает |
activeAttribute | string | null | null |
once | boolean | false |
threshold | number | number[] | — |
rootMargin | string | — |
root | HTMLElement | null | — |
onEnter / onLeave | (info: IntersectionInfo) => void | — |
Возвращаемое значение
Та же форма, что у Видимости элемента выше — isVisible, ratio.
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-адаптера — контекст вместо общего модульного объекта.
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()
function useInviewDefaults(): { threshold: number | number[]; rootMargin: string; once: boolean }Возвращает текущие значения <InviewProvider>, либо значения по умолчанию для всего пакета, если вызван вне провайдера — так существующий код без провайдера в дереве продолжает вести себя точно так же, как раньше.
<InView>
Render-prop-обёртка вокруг useElementVisibility для случаев, когда измеряемый элемент — это как раз тот, чей рендер зависит от результата (например, лениво монтировать тяжёлый дочерний компонент) — здесь useState-based callback-реф был бы лишним бойлерплейтом вокруг того же самого. Форма отличается от Vue-шного <InView>: children здесь — функция (render prop), а не scoped-слот.
import { InView } from '@macrulez/inview-react'
;<InView once>{({ isVisible }) => (isVisible ? <Heavy /> : null)}</InView>Пропсы
| Поле | Тип | По умолчанию |
|---|---|---|
once | boolean | значение по умолчанию из <InviewProvider> |
threshold | number | number[] | значение по умолчанию из <InviewProvider> |
rootMargin | string | значение по умолчанию из <InviewProvider> |
root | HTMLElement | null | null |
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.
function useElementViewport(
target: HTMLElement | null,
options?: UseElementViewportOptions,
): UseElementViewportReturnВозвращает ту же форму — rect, viewportProgress, distanceFromCenter — как обычные значения. options.root (по умолчанию null, окно) измеряет viewportProgress/distanceFromCenter относительно скроллящегося контейнера вместо окна — rect при этом всегда остаётся привязанным к окну.
Параллакс-слой
useParallaxLayer() — React-эквивалент Параллакс-слоя.
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 } |
progress | number |
Пример:
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, создающая контроллер в эффекте и уничтожающая его автоматически при размонтировании.
function useRevealController(options?: RevealControllerOptions): RevealControllerimport { useRevealController } from '@macrulez/inview-react'
function App() {
useRevealController({ stagger: { step: 70, max: 4 } })
return <>{/* ...карточки с className="reveal", включая добавленные асинхронной подгрузкой */}</>
}options захватываются один раз, при первом рендере, и используются на весь срок жизни контроллера — та же одноразовая модель опций, что и у самого createRevealController. Возвращаемые методы refresh/observe/unobserve/destroy проксируют к инстансу контроллера после того, как отработал эффект монтирования — вызывайте их из обработчика события или другого эффекта, а не из тела рендера.
Прямой вызов createRevealController в собственном эффекте по-прежнему работает точно так же для всего, что не покрывает обёртка:
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.
function usePrefersReducedMotion(): booleanimport { usePrefersReducedMotion } from '@macrulez/inview-react'
function Animated() {
const reduced = usePrefersReducedMotion()
return <div className={reduced ? undefined : 'spin'} />
}Задержка стаггера (useStagger)
useStagger() — тонкая обёртка вокруг staggerDelay() для паритета с Vue-адаптером; вычисление достаточно дешёвое (простая арифметика), чтобы просто вызывать его каждый рендер — useMemo не нужен.
function useStagger(index: number, options?: StaggerDelayOptions): stringimport { 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).
function useScrollTo(
target?: Window | HTMLElement | null,
): (el: Element | null | undefined, options?: UseScrollToOptions) => voidtarget захватывается один раз, в массиве зависимостей useCallback — передайте новый target, чтобы получить колбэк, привязанный к другому контейнеру. Возвращаемый колбэк — no-op, если el/target равны null.
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).
function useScrollSpy(
targets: Array<HTMLElement | null | undefined>,
options?: UseScrollSpyOptions,
): number | nulltargets должен быть стабильным массивом (например, из useState, а не свежий литерал [...] на каждый рендер) — новая ссылка на массив вызывает настоящую переподписку, так же как target/root в остальных местах пакета. options захватываются один раз, при изменении самого targets — та же одноразовая модель, что у useRevealController выше.
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 не нужна.