How I created the smallest stale-while-revalidate data fetching library that works everywhere
The Problem: Framework Lock-in and Bundle Bloat
As a developer who's worked across different JavaScript frameworks, I've always been frustrated by the ecosystem fragmentation around data fetching libraries. Want to use SWR patterns in React? Use swr (6KB). Need something similar in Vue? You'll need a different library. Working on a micro-frontend with multiple frameworks? Good luck maintaining consistency.
The popular solutions all have the same issues:
- Framework lock-in: Most libraries are tied to specific frameworks
- Bundle bloat: Even simple data fetching adds 6-13KB to your bundle
- Dependency hell: External dependencies create security and maintenance overhead
- Inconsistent APIs: Different libraries mean different learning curves
I wanted something different: a framework-agnostic, lightweight, zero-dependency solution that could work anywhere JavaScript runs.
Enter Revali: SWR for Everyone
Revali (from "revalidate") is my attempt to solve these problems. It's a complete SWR implementation in just 1.8KB gzipped with zero dependencies that works with any framework or vanilla JavaScript.
Key Design Principles
- Framework Agnostic: Works with React, Vue, Svelte, or plain JavaScript
- Minimal Bundle Impact: ~1.8KB gzipped vs 6-13KB for alternatives
- Zero Dependencies: No external packages, no security vulnerabilities
- TypeScript First: Built with strict TypeScript for maximum type safety
- Feature Complete: All the SWR features you expect
Technical Deep Dive
Architecture Overview
Revali is built around four core modules:
src/
├── core/
│ ├── cache.ts # LRU cache with TTL
│ ├── fetcher.ts # Request deduplication & retry logic
│ ├── subscription.ts # Pub/sub system for reactivity
│ ├── mutate.ts # Optimistic updates
│ └── revalidation.ts # Background revalidation
└── index.ts # Public API
The entire codebase is just 487 lines of TypeScript, yet it implements all the features you'd expect from a mature SWR library.
Smart Caching System
The cache implementation combines TTL (Time To Live) with LRU (Least Recently Used) eviction:
interface CacheEntry<T> {
data: T | undefined;
timestamp: number;
error?: Error;
fetcher: Fetcher<T>;
options: RevaliOptions;
}
class Cache {
private cache = new Map<string, CacheEntry<any>>();
private maxSize = 100;
get<T>(key: string): CacheEntry<T> | undefined {
const entry = this.cache.get(key);
if (entry) {
// Move to end (most recently used)
this.cache.delete(key);
this.cache.set(key, entry);
}
return entry;
}
set<T>(key: string, entry: CacheEntry<T>): void {
if (this.cache.size >= this.maxSize) {
// Remove least recently used
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, entry);
}
}
Request Deduplication
One of the most important features is preventing duplicate requests. If multiple components request the same data simultaneously, only one network request is made:
const pendingRequests = new Map<string, Promise<any>>();
export async function fetchWithDedup<T>(
key: string,
fetcher: Fetcher<T>,
options: RevaliOptions,
): Promise<T> {
// Check if request is already pending
if (pendingRequests.has(key)) {
return pendingRequests.get(key)!;
}
// Create new request
const promise = executeRequest(key, fetcher, options);
pendingRequests.set(key, promise);
try {
const result = await promise;
return result;
} finally {
pendingRequests.delete(key);
}
}
Subscription System
Revali uses a pub/sub pattern to notify subscribers when cache data changes:
type Subscriber<T> = (data: T | undefined, error?: Error) => void;
class SubscriptionManager {
private subscribers = new Map<string, Set<Subscriber<any>>>();
subscribe<T>(key: string, callback: Subscriber<T>) {
if (!this.subscribers.has(key)) {
this.subscribers.set(key, new Set());
}
this.subscribers.get(key)!.add(callback);
return () => this.unsubscribe(key, callback);
}
notify<T>(key: string, data: T | undefined, error?: Error) {
const callbacks = this.subscribers.get(key);
if (callbacks) {
callbacks.forEach(callback => {
try {
callback(data, error);
} catch (err) {
console.error('Subscriber error:', err);
}
});
}
}
}
This enables reactive updates across any UI framework.
Framework Integration
Vanilla JavaScript
The core API is simple and framework-agnostic:
import { revaliFetch, subscribe, mutate } from 'revali';
// Fetch with caching
const users = await revaliFetch(
'users',
() => fetch('/api/users').then(r => r.json()),
{ ttl: 300000 } // 5 minutes
);
// Subscribe to updates
const unsubscribe = subscribe('users', (data, error) => {
if (error) {
console.error('Error:', error);
} else {
updateUI(data);
}
});
// Optimistic updates
mutate('users', users => [...users, newUser]);
React Integration
Creating a React hook is straightforward:
import { useState, useEffect, useCallback } from 'react';
import { revaliFetch, subscribe, mutate } from 'revali';
function useRevali(key, fetcher, options) {
const [data, setData] = useState();
const [error, setError] = useState();
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
let mounted = true;
const loadData = async () => {
try {
const result = await revaliFetch(key, fetcher, options);
if (mounted) {
setData(result);
setError(undefined);
}
} catch (err) {
if (mounted) setError(err);
} finally {
if (mounted) setIsLoading(false);
}
};
loadData();
const unsubscribe = subscribe(key, (newData, newError) => {
if (!mounted) return;
setData(newData);
setError(newError);
setIsLoading(false);
});
return () => {
mounted = false;
unsubscribe();
};
}, [key]);
const mutateFn = useCallback((data, shouldRevalidate = true) => {
return mutate(key, data, shouldRevalidate);
}, [key]);
return { data, error, isLoading, mutate: mutateFn };
}
// Usage
function UserList() {
const { data: users, error, isLoading } = useRevali(
'users',
() => fetch('/api/users').then(r => r.json())
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Vue Integration
Vue integration using the Composition API:
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { revaliFetch, subscribe } from 'revali';
function useRevali(key, fetcher, options) {
const data = ref();
const error = ref();
const isLoading = ref(true);
let unsubscribe = null;
const load = async () => {
try {
data.value = await revaliFetch(key, fetcher, options);
error.value = null;
} catch (err) {
error.value = err;
} finally {
isLoading.value = false;
}
};
onMounted(async () => {
await load();
unsubscribe = subscribe(key, (newData, newError) => {
data.value = newData;
error.value = newError;
});
});
onUnmounted(() => {
unsubscribe?.();
});
return { data, error, isLoading };
}
const { data: users, error, isLoading } = useRevali(
'users',
() => fetch('/api/users').then(r => r.json())
);
</script>
<template>
<div v-if="isLoading">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<ul v-else>
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>
</ul>
</template>
Node.js Server-Side Usage
Revali also works great for server-side caching:
import { revaliFetch } from 'revali';
// Cache expensive database queries
const getUsers = async () => {
return revaliFetch(
'active-users',
() => db.query('SELECT * FROM users WHERE active = 1'),
{ ttl: 60000 } // 1 minute cache
);
};
// Cache external API calls
const getExchangeRate = async (currency) => {
return revaliFetch(
`exchange-rate-${currency}`,
() => fetch(`https://api.exchange.com/${currency}`).then(r => r.json()),
{ ttl: 300000 } // 5 minutes
);
};
Performance Benefits
Bundle Size Comparison
| Library | Bundle Size | Dependencies |
|---|---|---|
| SWR | ~6KB | Has dependencies |
| TanStack Query | ~13KB | Has dependencies |
| Revali | ~1.8KB | Zero dependencies |
Real-World Impact
In a typical React application:
- Before: SWR + dependencies = ~8KB added to bundle
- After: Revali = ~1.8KB added to bundle
- Savings: ~77% reduction in bundle size
For mobile users on slow networks, this translates to:
- Faster initial page loads
- Better Core Web Vitals scores
- Improved user experience
Advanced Features
Error Handling with Exponential Backoff
const data = await revaliFetch(
'critical-data',
fetchCriticalData,
{
retries: 3,
retryDelay: 1000, // Start with 1s
// Automatically increases: 1s → 1.5s → 2.25s
}
);
Automatic Revalidation
const data = await revaliFetch(
'user-profile',
fetchUserProfile,
{
revalidateOnFocus: true, // Refresh when tab becomes active
revalidateOnReconnect: true, // Refresh when network reconnects
}
);
Cache Introspection
import { getCacheInfo, clearCache } from 'revali';
// Debug cache state
console.log(getCacheInfo());
// { size: 5, keys: ['users', 'posts', ...] }
// Clear specific cache
clearCache('users');
// Clear all cache
clearCache();
Testing and Quality
Revali has comprehensive test coverage with 108 test cases across:
- Unit tests for core functionality
- Integration tests for real-world scenarios
- Error handling and edge cases
- Performance benchmarks
npm run test:coverage
# Functions: 89.28% coverage
# All core features tested
Development Experience
TypeScript-First Design
Every API is fully typed with strict TypeScript:
interface RevaliOptions {
retries?: number;
retryDelay?: number;
ttl?: number;
maxCacheSize?: number;
revalidateOnFocus?: boolean;
revalidateOnReconnect?: boolean;
}
function revaliFetch<T>(
key: string,
fetcher: () => Promise<T>,
options?: RevaliOptions
): Promise<T>;
Tree Shaking Support
Only import what you need:
// Only includes revaliFetch and mutate in bundle
import { revaliFetch, mutate } from 'revali';
Modern Development Workflow
- GitHub Actions CI/CD
- Automated releases with changesets
- Comprehensive examples and documentation
- ESLint + Prettier for code quality
Challenges and Solutions
Challenge 1: Framework Agnosticism
Problem: How to provide reactivity without framework-specific code?
Solution: Pub/sub pattern with manual subscription management. Each framework can implement its own reactive wrapper.
Challenge 2: Bundle Size
Problem: How to implement all SWR features in minimal code?
Solution:
- Zero dependencies
- Careful API design
- Modular architecture for tree shaking
- TypeScript for compile-time optimization
Challenge 3: Request Deduplication
Problem: Preventing duplicate requests across different components/calls.
Solution: Global pending request map with promise sharing.
Challenge 4: Memory Management
Problem: Preventing memory leaks in long-running applications.
Solution: LRU cache with configurable limits and automatic cleanup.
Roadmap and Future Plans
Current Status (v0.1.0)
- ✅ Core SWR functionality
- ✅ Framework-agnostic design
- ✅ Zero dependencies
- ✅ TypeScript support
- ✅ Comprehensive testing
Coming Soon (v0.2.0)
- 🚧 Built-in React/Vue/Svelte hooks
- 🚧 Polling and interval revalidation
- 🚧 Request cancellation (AbortController)
- 🚧 Middleware system
Future (v0.3.0+)
- 🔮 DevTools browser extension
- 🔮 GraphQL integration
- 🔮 SSR/SSG support
- 🔮 Offline support with persistence
Lessons Learned
1. Less is More
Starting with a minimal API forced better design decisions. Every feature had to justify its existence and bundle cost.
2. TypeScript Pays Off
Building with strict TypeScript from day one prevented countless bugs and made refactoring safe and fast.
3. Testing Early Matters
Writing tests alongside features caught edge cases early and made the library more robust.
4. Documentation is Code
Good examples and documentation are as important as the code itself for adoption.
Try Revali Today
Revali is ready for production use. Here's how to get started:
npm install revali
import { revaliFetch } from 'revali';
const data = await revaliFetch(
'my-data',
() => fetch('/api/data').then(r => r.json()),
{ ttl: 300000 }
);
Resources
- GitHub: github.com/cerebralatlas/revali
- npm: npmjs.com/package/revali
- Documentation: Full API docs and examples in the repo
- Examples: Vanilla JS, Node.js, and framework integration examples
Conclusion
Building Revali taught me that sometimes the best solution isn't the most feature-rich one—it's the one that solves real problems with the least complexity.
In a world of framework lock-in and bundle bloat, Revali offers a different path: universal compatibility, minimal overhead, and zero dependencies. Whether you're building a React app, a Vue project, a Svelte site, or a vanilla JavaScript application, Revali provides consistent, reliable data fetching.
The 1.8KB bundle size isn't just a nice-to-have—it's a statement about what's possible when we prioritize simplicity and focus on solving real problems.
Give Revali a try in your next project. I think you'll find that sometimes, less really is more.
If you found this interesting, give Revali a star on GitHub and let me know what you think! I'm always looking for feedback and contributions from the community.
Links: