Revali: A 1.8KB Framework-Agnostic SWR Library

September 9, 2025

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

  1. Framework Agnostic: Works with React, Vue, Svelte, or plain JavaScript
  2. Minimal Bundle Impact: ~1.8KB gzipped vs 6-13KB for alternatives
  3. Zero Dependencies: No external packages, no security vulnerabilities
  4. TypeScript First: Built with strict TypeScript for maximum type safety
  5. 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

LibraryBundle SizeDependencies
SWR~6KBHas dependencies
TanStack Query~13KBHas dependencies
Revali~1.8KBZero 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

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: