Skip to content

Decouple lenis from the main entry so non-scroll consumers don't need it #15

Description

@arzafran

Context

In #12 (v1.0.0) lenis was made a required peer dependency. A cross-model (Codex) review caught that the main hamo entry statically re-exports useScrollTrigger (src/index.ts), which statically does import { useLenis } from 'lenis/react'. So the main entry's module graph always pulls in lenis/react — declaring lenis optional was not a robust guarantee (it could fail to resolve in Vite dev / SSR / non-tree-shaking paths for a consumer who only uses, say, useWindowSize and never installed lenis).

The v1 fix was the minimal one — make lenis required — which is correct but heavier than ideal: every hamo consumer must now install lenis, even if they only use the non-scroll hooks (useRect, useWindowSize, useMediaQuery, useObjectFit, …). This issue tracks decoupling lenis so it's only a dependency for consumers who actually use scroll triggers.

Goal

A consumer using only non-scroll hooks should not need lenis installed, while useScrollTrigger keeps its Lenis integration (with the native-scroll fallback intact). Pick one of the variants below.

Variants

A. Subpath export — hamo/scroll-trigger (recommended)

Move useScrollTrigger / useTransform (and the existing hamo/scroll-trigger/debugger) out of the root barrel into a dedicated subpath. The main hamo entry then never imports lenis/react, so lenis goes back to a genuinely optional peer — only pulled when you import hamo/scroll-trigger.

  • ✅ Cleanest separation; mirrors how the debugger subpath already works; matches sideEffects: false intent without relying on tree-shaking.
  • ⚠️ Breaking import path for the scroll hooks (hamohamo/scroll-trigger). Cheap to do now-ish since the hooks are new in v1; would be a major bump later.

B. Dependency injection — pass Lenis in, don't import it

useScrollTrigger stops importing lenis/react and instead receives the Lenis instance (or a useLenis-like accessor) via options or a small context the consumer wires up. The static dependency disappears entirely.

  • ✅ Removes the hard lenis/react import; lenis becomes a peer only the consumer's app references.
  • ⚠️ API change to useScrollTrigger; more wiring for the common case; need a clean default that still "just works" with ReactLenis.

C. Dynamic import of lenis

Lazy-load lenis/react inside useScrollTrigger.

  • ⚠️ Likely not viable: useLenis is a hook and can't be conditionally/dynamically imported without violating rules-of-hooks. Documented here only to rule it out.

D. Status quo — keep lenis required

No decoupling. Simplest, but every consumer carries lenis.

Acceptance criteria

  • Importing only non-scroll hooks from hamo resolves with no lenis installed (verify in a Vite dev app and an SSR/RSC build, not just a tree-shaken prod bundle — that's the path publint/attw/local builds miss because lenis is in devDeps).
  • useScrollTrigger still integrates with Lenis and still falls back to native scroll when no Lenis instance is mounted.
  • lenis is declared optional again (peerDependenciesMeta) if variant A/B is chosen.
  • publint clean; attw sane; CHANGELOG + READMEs updated.

Notes

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions