@ultralab/react-hooks
April 3, 2026 · View on GitHub
4 React hooks that replace 4 npm packages. Zero deps. ~3KB total. Production-tested on ultralab.tw.
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-queryif 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
- ultralab.tw — AI Product Studio
- UltraProbe — Free AI security scanner (built with these hooks)
- Discord — Developer community