hamo

repository·main·Indexed 43 days ago

https://github.com/darkroomengineering/hamo

A collection of performance-oriented, render-safe React hooks for measurement, debouncing, and DOM observation. Features include useDebouncedState, useDebouncedEffect, useDebouncedCallback, a stable useEffectEvent polyfill, useIntersectionObserver, and useLazyState. Version 1.0.3.

Tokens
18.1K
Snippets
55
Records
73
Agent score
54%

What's inside hamo

  1. Overview of hamo hooks

    main

    hāmō is a collection of render-safe, tree-shakeable, and performance-oriented React hooks. The library provides specialized tools for measurement, debouncing, and DOM observation without causing layout thrash.

    Core Hook Categories:

    • Measurement Hooks: Track element and viewport dimensions using useRect, useWindowSize, and useResizeObserver.
    • Intersection & Scroll: Observe visibility and scroll progress with useIntersectionObserver and useScrollTrigger.
    • Debouncing: Manage timing for callbacks, effects, and state using useDebouncedCallback, useDebouncedEffect, and useDebouncedState.
    • State & Logic: Optimize updates with useLazyState (update without re-rendering) and useEffectEvent (stable function calling the latest callback).
    • Layout & Transforms: Compute scaling with useObjectFit and manage tree-based transformations with useTransform.
  2. Use the useObjectFit hook to scale child elements

    main
    The useObjectFit hook is a React hook designed to calculate the necessary scale factor for a child element so that it fits within the boundaries of a parent element. This is useful for implementing custom 'object-fit' behaviors (like contain or cover) when working with non-image elements or complex layouts where CSS object-fit is insufficient.
  3. How `<ReactLenis>` and `useLenis` work together

    main

    The lenis/react package uses a Provider pattern to manage smooth scrolling.

    1. <ReactLenis>: This component creates a Lenis instance and provides it to its children via React Context. Wrapping your application (or a specific scroll container) in <ReactLenis> enables smooth scrolling for that section.
    2. useLenis: This hook allows any child component within the <ReactLenis> tree to access the Lenis instance directly without prop drilling.

    To enable Lenis on the main window/document scroll, pass the root prop to <ReactLenis>.

    import { ReactLenis, useLenis } from 'lenis/react'
    
    function Component() {
      // Access the instance and listen to scroll events
      const lenis = useLenis(({ scroll }) => {
        // called every scroll
      })
    
      return (
        <ReactLenis root>
          { /* content */ }
        </ReactLenis>
      )
    }
  4. How useTransform and TransformProvider work together

    main

    The useTransform system is a context-based accumulation mechanism for programmatic transforms (translate, rotate, scale).

    • TransformProvider wraps a subtree and tracks its own transform state. When providers are nested, they accumulate values: translate and rotate are additive, while scale is multiplicative.
    • useTransform allows child components to either read the current accumulated transform on demand or register a callback to react to changes in any ancestor provider.

    This is primarily used for scroll trigger compensation, ensuring that child components (like those using useScrollTrigger) can account for parent offsets (e.g., parallax movements) to compute accurate progress values.

    import { useRef } from 'react'
    import { TransformProvider, useTransform } from 'hamo'
    import type { TransformRef } from 'hamo'
    
    function ParallaxSection() {
      const transformRef = useRef<TransformRef>(null)
    
      function onScroll(y) {
        transformRef.current?.setTranslate(0, y)
      }
    
      return (
        <TransformProvider ref={transformRef}>
          <ChildComponent />
        </TransformProvider>
      )
    }
    
    function ChildComponent() {
      const getTransform = useTransform()
      const { translate } = getTransform()
    
      useTransform((transform) => {
        console.log('Parent moved to:', transform.translate.y)
      })
    
      return <div>Content</div>
    }
  5. Use useEffectEvent to access latest state/props without re-triggering effects

    main

    The useEffectEvent hook is a polyfill for React's experimental useEffectEvent. It returns a stable function reference that always calls the latest version of your provided callback. This allows you to use the latest props or state inside an effect without including those values in the effect's dependency array, preventing unnecessary re-runs.

    Key Characteristics

    • Stable Identity: The returned function reference never changes across renders.
    • Latest Values: When called, it always invokes the callback from the most recent render.
    • Dependency Safety: You can safely omit the returned function from useEffect dependency arrays.

    Comparison with React's Experimental Version

    FeatureReact useEffectEventhamo useEffectEvent
    AvailabilityExperimental (not in stable)React 17+
    IdentityIntentionally unstableStable (same reference every render)
    UsageEffects and Effect Events onlyAnywhere (effects, event handlers, etc.)
    LintingESLint plugin enforces constraintsNo restrictions
    import { useEffectEvent } from 'hamo'
    
    function Chat({ url, onMessage }) {
      const handleMessage = useEffectEvent(onMessage)
    
      useEffect(() => {
        const ws = new WebSocket(url)
        ws.addEventListener('message', handleMessage)
    
        return () => ws.close()
      }, [url]) // handleMessage doesn't need to be in deps
    }
  6. Compensate for parent transforms with `TransformProvider`

    main

    Because useScrollTrigger caches element positions to avoid layout thrashing, programmatic transforms on a parent element (like parallax translateY) will make the cached positions stale.

    To fix this, wrap the parent in a TransformProvider and pass a TransformRef to it. You must then call .setTranslate(x, y) on that ref whenever the parent moves. Child scroll triggers inside this provider will automatically compensate for the offset, ensuring accurate progress tracking without needing to re-read the DOM.

    import { useRef } from 'react'
    import { useScrollTrigger, TransformProvider } from 'hamo'
    import type { TransformRef } from 'hamo'
    
    function ParallaxWrapper({ children }) {
      const transformRef = useRef<TransformRef>(null)
      const elementRef = useRef(null)
    
      const [setRef] = useScrollTrigger({
        onProgress: ({ progress }) => {
          const y = (progress - 0.5) * -100
          transformRef.current?.setTranslate(0, y)
          elementRef.current.style.transform = `translateY(${y}px)`
        },
      })
    
      return (
        <TransformProvider ref={transformRef}>
          <div ref={(node) => { elementRef.current = node; setRef(node) }}>
            {children}
          </div>
        </TransformProvider>
      )
    }
    
    function ChildTrigger() {
      const [setRef] = useScrollTrigger({
        onProgress: ({ progress }) => {
          // Accurate despite parent parallax
          console.log('progress:', progress)
        },
      })
    
      return <div ref={setRef}>Child content</div>
    }
    
    function Page() {
      return (
        <ParallaxWrapper>
          <ChildTrigger />
        </ParallaxWrapper>
      )
    }
  7. Understand `useScrollTrigger` position syntax

    main

    Positions are defined using the format: "element-position viewport-position".

    Keywords:

    • top: Top edge
    • center: Vertical center
    • bottom: Bottom edge
    • number: Pixel offset from top

    Common Combinations:

    • 'bottom bottom' / 'top top': Full element traversal (default).
    • 'bottom bottom' / 'top center': From entering viewport to reaching center.
    • 'center center' / 'top top': From center alignment to reaching top.
  8. Use the useIntersectionObserver hook

    main

    The useIntersectionObserver hook allows you to observe the visibility of a DOM element using the browser's IntersectionObserver API.

    It returns a tuple containing a ref-setter function and the current IntersectionObserverEntry. You attach the returned setElement function to a ref on the element you wish to monitor.

    import { useIntersectionObserver } from 'hamo'
    
    function App() {
      // setElement is used as a ref, entry contains the visibility state
      const [setElement, entry] = useIntersectionObserver()
    
      console.log(entry?.isIntersecting)
    
      return <div ref={setElement} />
    }
  9. Integrate Lenis with custom animation loops

    main

    By default, Lenis handles its own requestAnimationFrame (RAF) loop. However, if you want to synchronize Lenis with an external animation engine or a custom loop, set options={{ autoRaf: false }} and manually call lenis.raf(time) using a ref to the <ReactLenis> component.

    Custom requestAnimationFrame loop

    import { ReactLenis } from 'lenis/react'
    import { useEffect, useRef } from 'react'
    
    function Component() {
      const lenisRef = useRef()
      
      useEffect(() => {
        function update(time) {
          lenisRef.current?.lenis?.raf(time)
        }
      
        const rafId = requestAnimationFrame(update)
      
        return () => cancelAnimationFrame(rafId)
      }, [])
      
      return (
        <ReactLenis options={{ autoRaf: false }} ref={lenisRef}>
          { /* content */ }
        </ReactLenis>
      )
    }

    GSAP integration

    import gsap from 'gsap'
    import { ReactLenis } from 'lenis/react'
    import { useEffect, useRef } from 'react'
    
    function Component() {
      const lenisRef = useRef()
      
      useEffect(() => {
        function update(time) {
          // GSAP ticker uses milliseconds, so multiply time by 1000
          lenisRef.current?.lenis?.raf(time * 1000)
        }
      
        gsap.ticker.add(update)
      
        return () => gsap.ticker.remove(update)
      }, [])
      
      return (
        <ReactLenis options={{ autoRaf: false }} ref={lenisRef}>
          { /* content */ }
        </ReactLenis>
      )
    }

    Framer Motion integration

    import { ReactLenis } from 'lenis/react'
    import { cancelFrame, frame } from 'framer-motion';
    import { useEffect, useRef } from 'react';
    
    function Component() {
      const lenisRef = useRef()
    
      useEffect(() => {
        function update(time) {
          lenisRef.current?.lenis?.raf(time)
        }
    
        frame.update(update, true)
    
        return () => cancelFrame(update)
      }, [])
    
      return (
        <ReactLenis options={{ autoRaf: false }} ref={lenisRef}>
          { /* content */ }
        </ReactLenis>
      )
    }
  10. How TransformProvider and useTransform work together

    main

    The TransformProvider manages coordinate transformations (translation, rotation, and scale) in a composable hierarchy. It is designed to allow nested providers to accumulate transforms, which is useful for effects like parallax where child components need to account for parent offsets.

    Accumulation Logic:

    • Translate and Rotate: These are additive (e.g., parent y: 10 + child y: 5 = y: 15).
    • Scale: This is multiplicative (e.g., parent scale: 2 * child scale: 0.5 = scale: 1).
    • userData: These are merged objects.

    To use it, wrap a component tree in a TransformProvider. You can control the transform via a ref attached to the provider or by using the useTransform hook in child components to react to changes.

    import { TransformProvider, useTransform } from 'hamo'
    import { useRef } from 'react'
    
    function ParallaxWrapper({ children }) {
      const ref = useRef<TransformRef>(null)
    
      // Update transform on scroll (example using a hypothetical useScrollTrigger)
      useScrollTrigger({
        onProgress: ({ progress }) => {
          ref.current?.setTranslate(0, progress * -100)
        },
      })
    
      return (
        <TransformProvider ref={ref}>
          {children}
        </TransformProvider>
      )
    }