Skip to content
This repository was archived by the owner on Apr 3, 2026. It is now read-only.

ppcvote/ultralab-react-hooks

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@ultralab/react-hooks

4 React hooks that replace 4 npm packages. Zero deps. ~3KB total. Production-tested on ultralab.tw.

npm bundle size license

Why This Exists

You need scroll animations, so you install react-intersection-observer (14KB). You need meta tags, so you install react-helmet (12KB). You need number counters, so you install react-countup (44KB). You need data fetching, so you install swr (41KB).

That's 4 packages, 111KB, 4 node_modules subtrees for basic UI patterns.

Or:

npm install @ultralab/react-hooks   # ~3KB. Zero deps. All four.

What Replaces What

This hook Replaces Their size Our size
useInView react-intersection-observer 14KB ~0.4KB
useMeta react-helmet / react-helmet-async 12KB ~0.8KB
useCountUp react-countup 44KB ~0.3KB
useCachedFetch swr / react-query (basic) 41KB ~0.5KB
Total 4 packages 111KB ~3KB

The Killer Feature: useMeta Without React Router

Most meta tag libraries assume you use React Router or Next.js. We don't.

useMeta was built for SPAs that route with window.location.pathname + React.lazy() (like ultralab.tw — 10+ pages, zero React Router). It updates <title>, <meta>, <link canonical>, OG tags, and Twitter Cards on mount, and restores them on unmount.

No SSR required. No router dependency. Just works.


Install

npm install @ultralab/react-hooks

Hooks

useInView — Scroll-triggered animations

Tracks whether an element is visible using IntersectionObserver. Disconnects after first trigger by default.

import { useInView } from '@ultralab/react-hooks'

function FadeInSection({ children }) {
  const { ref, isInView } = useInView({ threshold: 0.2 })
  return (
    <div
      ref={ref}
      className={isInView ? 'animate-fade-in-up' : 'opacity-0'}
    >
      {children}
    </div>
  )
}
Option Type Default Description
threshold number 0.1 Visibility ratio to trigger
rootMargin string '0px' CSS margin around root
triggerOnce boolean true Fire once then disconnect

Stagger pattern:

{items.map((item, i) => (
  <div
    className={isInView ? 'animate-fade-in-up' : 'opacity-0'}
    style={{ animationDelay: `${i * 0.1}s` }}
  >
    {item.title}
  </div>
))}

useCountUp — Animated number counter

Ease-out cubic animation from 0 to target. Respects prefers-reduced-motion.

import { useInView, useCountUp } from '@ultralab/react-hooks'

function Stats() {
  const { ref, isInView } = useInView()
  const users = useCountUp(12500, 2000, isInView)
  const revenue = useCountUp(47800, 1500, isInView)

  return (
    <div ref={ref}>
      <span>{users.toLocaleString()} users</span>
      <span>${revenue.toLocaleString()} MRR</span>
    </div>
  )
}
Param Type Default Description
end number Target number
duration number 2000 Animation ms
start boolean true When to begin

useMeta — SPA meta tag manager (no Router needed)

Updates <title>, description, canonical, OG, Twitter Card. Restores on unmount.

import { useMeta } from '@ultralab/react-hooks'

function ProductPage() {
  useMeta({
    title: 'UltraProbe — AI Security Scanner',
    description: 'Scan your AI prompts for 12 attack vectors in 5 seconds.',
    canonical: 'https://ultralab.tw/probe',
    ogImage: 'https://ultralab.tw/ultraprobe-og.png',
    themeColor: '#0A0A12',
  })
  return <main>...</main>
}
Field Type Required Description
title string Yes Page title
description string Yes Meta description
canonical string Yes Canonical URL
ogTitle string No OG title (defaults to title)
ogDescription string No OG description
ogImage string No OG image URL
ogType string No OG type (website, article)
themeColor string No Theme color meta

How it works:

Mount:   saves current meta → applies your config
Unmount: restores saved meta (navigation "back" works)

No context provider. No <Helmet> wrapper. Just call the hook.


useCachedFetch — Deduplicated data fetching

Module-level cache. Multiple components requesting the same URL = single fetch.

import { useCachedFetch } from '@ultralab/react-hooks'

function Dashboard() {
  const { data, loading } = useCachedFetch({
    url: '/api/stats',
    fallback: { users: 0, revenue: 0 },
    transform: (raw: any) => ({
      users: raw.total_users,
      revenue: raw.monthly_revenue,
    }),
  })

  if (loading) return <Skeleton />
  return <StatsGrid data={data} />
}
Field Type Required Description
url string Yes GET endpoint
fallback T Yes Value while loading / on error
transform (data: unknown) => T No Transform raw JSON

Not a replacement for react-query if you need mutations, pagination, or revalidation. But for simple GET + cache — ~0.5KB vs 41KB.


Production Usage

These hooks power ultralab.tw:

  • 10+ SPA pages routed via window.location.pathname (no React Router)
  • Scroll animations on every section
  • Dynamic meta tags per page (SEO-optimized, crawlers see correct metadata)
  • Real-time stats dashboard
  • 7,500+ monthly active users

Requirements

  • React >= 17
  • TypeScript (optional but recommended)

License

MIT — Ultra Lab

Links

About

4 lightweight React hooks — scroll animations, number counters, SPA meta tags, cached fetch. Zero deps, fully typed.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors