4 React hooks that replace 4 npm packages. Zero deps. ~3KB total. Production-tested on ultralab.tw.
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.| 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 |
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.
npm install @ultralab/react-hooksTracks 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>
))}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 |
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.
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.
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
- React >= 17
- TypeScript (optional but recommended)
MIT — Ultra Lab
- ultralab.tw — AI Product Studio
- UltraProbe — Free AI security scanner (built with these hooks)
- Discord — Developer community