npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jolyui/swing

v1.0.0

Published

Lightweight, powerful data fetching for React. Simple by design, tiny by nature.

Downloads

10

Readme

@jolyui/swing

Lightweight, powerful data fetching for React

npm version bundle size license


Why Swing?

Swing is not another clone of existing data fetching libraries. It's engineered from the ground up with a focus on:

  • 🪶 Tiny - ~3KB minified + gzipped (core), ~5KB with React hooks
  • Fast - LRU cache, request deduplication, stale-while-revalidate
  • 🎯 Simple - Intuitive API, minimal configuration required
  • 💪 Powerful - Full TypeScript, Suspense, infinite scroll, mutations
  • 🔌 Flexible - Works with or without React, tree-shakeable

Installation

# npm
npm install @jolyui/swing

# pnpm
pnpm add @jolyui/swing

# yarn
yarn add @jolyui/swing

Quick Start

Basic Usage

import { useSwing } from '@jolyui/swing';

function UserProfile({ userId }) {
  const { data, isLoading, error } = useSwing(`/api/users/${userId}`);

  if (isLoading) return <Spinner />;
  if (error) return <Error message={error.message} />;

  return <div>{data.name}</div>;
}

With Configuration

import { useSwing } from '@jolyui/swing';

function SearchResults({ query }) {
  const { data, isLoading, refetch } = useSwing('/api/search', {
    params: { q: query },
    enabled: query.length > 0,
    cache: 'stale-while-revalidate',
    cacheTTL: 60000, // 1 minute
    revalidateOnFocus: true,
    retry: { count: 3 },
  });

  return (
    <div>
      {isLoading ? <Spinner /> : <Results items={data} />}
      <button onClick={refetch}>Refresh</button>
    </div>
  );
}

Create a Client

import { createClient, SwingProvider } from '@jolyui/swing';

// Create a configured client
const api = createClient({
  baseURL: 'https://api.example.com',
  headers: {
    'Authorization': `Bearer ${token}`,
  },
  timeout: 10000,
  retry: true,
});

// Use in your app
function App() {
  return (
    <SwingProvider 
      client={api}
      initialCache={window.__SWING_CACHE__} // SSR Hydration
    >
      <MyApp />
    </SwingProvider>
  );
}

Persistence & Offline

Enable automatic cache persistence to localStorage:

const api = createClient({
  persist: true,
  storageKey: 'my-app-cache', // optional
});

API Reference

Hooks

useSwing<T>(url, options?)

Main data fetching hook.

const {
  data,        // Response data
  error,       // Error if request failed
  isLoading,   // Initial loading state
  isValidating,// Revalidating in background
  isSuccess,   // Request succeeded
  isError,     // Request failed
  isStale,     // Data is from cache and may be stale
  refetch,     // Manually trigger refetch
  mutate,      // Optimistically update data
} = useSwing<User>('/api/user', {
  // Request options
  method: 'GET',
  headers: { 'X-Custom': 'value' },
  params: { page: 1 },
  timeout: 5000,
  
  // Cache options
  cache: 'cache-first',
  cacheTTL: 300000,
  cacheKey: 'custom-key',
  tags: ['users'],
  
  // Behavior options
  enabled: true,
  keepPreviousData: false,
  revalidateOnFocus: true,
  revalidateOnReconnect: true,
  pollingInterval: 30000,
  
  // Callbacks
  onSuccess: (data) => console.log('Success!', data),
  onError: (error) => console.error('Error!', error),
  onSettled: (data, error) => console.log('Done'),
});

useMutation<TData, TVariables>(mutationFn, options?)

For creating, updating, deleting data.

const {
  data,
  error,
  isLoading,
  isSuccess,
  isError,
  mutate,      // Fire and forget
  mutateAsync, // Returns promise
  reset,       // Reset state
} = useMutation(
  async (newUser: CreateUserInput) => {
    const response = await api.post<User>('/api/users', newUser);
    return response.data;
  },
  {
    onMutate: (variables) => {
      // Optimistic update
      return { previousData };
    },
    onSuccess: (data, variables, context) => {
      toast.success('User created!');
    },
    onError: (error, variables, context) => {
      // Rollback
    },
    invalidateOnSuccess: ['users'],
  }
);

// Usage
<button onClick={() => mutate({ name: 'John' })}>
  Create User
</button>

useSwingMutation<TData, TVariables>(url, method, options?)

Shorthand for common mutations.

const createUser = useSwingMutation<User, CreateUserInput>(
  '/api/users',
  'POST',
  { invalidateOnSuccess: ['users'] }
);

useInfinite<T, TPageParam>(getUrl, options?)

For infinite scrolling and pagination.

const {
  data,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage,
} = useInfinite<Page, string>(
  (cursor) => `/api/posts?cursor=${cursor}`,
  {
    getNextPageParam: (lastPage) => lastPage.nextCursor,
    initialPageParam: '',
  }
);

// Flatten pages
const posts = data?.pages.flatMap(page => page.items) ?? [];

return (
  <div>
    {posts.map(post => <Post key={post.id} {...post} />)}
    {hasNextPage && (
      <button onClick={fetchNextPage} disabled={isFetchingNextPage}>
        {isFetchingNextPage ? 'Loading...' : 'Load More'}
      </button>
    )}
  </div>
);

Cache Strategies

| Strategy | Description | |----------|-------------| | cache-first | Return cache immediately, fetch in background | | network-first | Try network, fallback to cache on error | | cache-only | Only return cached data, never fetch | | network-only | Always fetch, never use cache | | stale-while-revalidate | Return stale cache, revalidate in background |

Core Functions (React-free)

import { swing, createClient, get, post } from '@jolyui/swing/core';

// Direct fetch
const response = await swing<User>('/api/user');

// Using client
const api = createClient({ baseURL: 'https://api.example.com' });
const users = await api.get<User[]>('/users');
const newUser = await api.post<User>('/users', { name: 'John' });

// Shorthand
const data = await get<User>('/api/user');
await post('/api/users', { name: 'John' });

Utility Hooks

import {
  useOnline,
  useFocus,
  useDebounce,
  useDebouncedCallback,
  useInterval,
} from '@jolyui/swing';

// Online status
const isOnline = useOnline();

// Document focus
const isFocused = useFocus();

// Debounced value
const debouncedSearch = useDebounce(search, 300);

// Debounced callback
const debouncedFetch = useDebouncedCallback(fetchData, 300);

// Polling
useInterval(() => refetch(), isEnabled ? 5000 : null);

Cache Management

import { invalidateCache, clearCache, getGlobalCache } from '@jolyui/swing';

// Invalidate by key
invalidateCache('/api/users');

// Invalidate by tags
invalidateCache(['users', 'admin']);

// Clear all cache
clearCache();

// Access cache directly
const cache = getGlobalCache();
cache.set('custom', createCacheEntry(data, 60000, ['custom']));

// Advanced cache features
const extendedCache = getGlobalCache();
console.log(extendedCache.stats()); // { hits, misses, sets, deletes, evictions, gcRuns }
extendedCache.gc();                  // Run garbage collection
extendedCache.prune();               // Remove all stale entries
extendedCache.startGC(60000);        // Auto GC every minute
extendedCache.stopGC();              // Stop auto GC

// Serialize/deserialize cache for persistence
import { serializeCache, deserializeCache } from '@jolyui/swing';
localStorage.setItem('cache', serializeCache());
deserializeCache(localStorage.getItem('cache'));

DevTools

Swing includes optional DevTools for development debugging.

import { SwingDevTools, enableConsoleLogging } from '@jolyui/swing';

function App() {
  return (
    <>
      <MyApp />
      {process.env.NODE_ENV === 'development' && (
        <SwingDevTools 
          position="bottom-right"  // or bottom-left, top-right, top-left
          defaultOpen={false}
          maxRequests={50}
          maxEvents={100}
        />
      )}
    </>
  );
}

// Or enable console logging
if (process.env.NODE_ENV === 'development') {
  enableConsoleLogging();
}

DevTools features:

  • 📊 Cache Stats - Hits, misses, evictions, GC runs
  • 🔑 Cache Keys - View and clear cached entries
  • 📡 Request Log - Track all requests with timing and status
  • 📝 Event Stream - Debug all internal events

Event System

Subscribe to internal events for advanced use cases:

import { emitter } from '@jolyui/swing';

// Listen to cache events
emitter.on('cache:hit', ({ key, stale }) => {
  analytics.track('cache_hit', { key, stale });
});

emitter.on('cache:miss', ({ key }) => {
  analytics.track('cache_miss', { key });
});

// Listen to request events
emitter.on('request:start', ({ url, method }) => {
  console.log(`Starting ${method} ${url}`);
});

emitter.on('request:success', ({ url, status, duration, fromCache }) => {
  metrics.recordLatency(url, duration);
});

emitter.on('request:error', ({ url, error, attempt }) => {
  console.error(`Request failed (attempt ${attempt}):`, url, error);
});

emitter.on('request:retry', ({ url, attempt, delay }) => {
  console.log(`Retrying ${url} in ${delay}ms (attempt ${attempt})`);
});

// Listen to mutation events
emitter.on('mutation:success', ({ data, variables }) => {
  console.log('Mutation succeeded', { data, variables });
});

Debug Mode

Enable debug logging for development:

import { setDebug, logger } from '@jolyui/swing';

// Enable debug mode
setDebug(true);

// Use the logger directly
logger.debug('Custom debug message');
logger.info('Info message');
logger.warn('Warning message');
logger.error('Error message');

TypeScript

Swing is written in TypeScript and provides full type inference.

interface User {
  id: number;
  name: string;
  email: string;
}

// Type is inferred
const { data } = useSwing<User>('/api/user');
//     ^? User | undefined

// With mutation
const createUser = useMutation<User, { name: string }>(
  (input) => api.post<User>('/users', input).then(r => r.data)
);

Why Swing Over Others?

The Problem with Existing Solutions

TanStack Query (React Query) is incredibly powerful, but:

  • 🐘 Heavy - ~13KB min+gzip, plus requires a QueryClient provider
  • 📚 Complex - Steep learning curve with QueryClient, QueryCache, queryKey arrays
  • ⚙️ Over-engineered - Most apps don't need devtools, persistence, or offline sync
  • 🔧 Boilerplate - Requires wrapping your app, configuring clients, defining query keys

SWR is lighter, but:

  • 🔗 React-locked - No way to use the core fetching logic outside React
  • 🎯 Limited mutations - Mutation support feels like an afterthought
  • 📦 Still heavy for simple needs - ~4KB for basic data fetching

Swing's Philosophy: Simplicity Without Sacrifice

// TanStack Query - Lots of setup
const queryClient = new QueryClient();
function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MyComponent />
    </QueryClientProvider>
  );
}
function MyComponent() {
  const { data } = useQuery({
    queryKey: ['users', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
  });
}

// SWR - Simpler but still needs fetcher
const fetcher = (url) => fetch(url).then(r => r.json());
function MyComponent() {
  const { data } = useSWR(`/api/users/${userId}`, fetcher);
}

// Swing - Just works™
function MyComponent() {
  const { data } = useSwing(`/api/users/${userId}`);
}

Key Differentiators

| Aspect | Swing | TanStack Query | SWR | |--------|-------|----------------|-----| | Setup Required | None | QueryClient + Provider | Global config or fetcher | | Bundle Impact | ~3KB core, ~5KB with hooks | ~13KB + devtools | ~4.5KB | | Learning Curve | 5 minutes | 1-2 hours | 30 minutes | | Query Keys | Automatic from URL | Manual arrays required | Automatic from URL | | Built-in Fetch | ✅ Yes | ❌ Bring your own | ❌ Bring your own | | React Optional | ✅ Use core anywhere | ❌ React only | ❌ React only | | Interceptors | ✅ Request & response | ❌ External | ❌ External | | Type Inference | ✅ Automatic | ⚠️ Needs generics | ⚠️ Needs generics |

When to Use What

Choose Swing if you:

  • Want the simplest possible API
  • Care about bundle size
  • Need to use fetching logic outside React (Node.js, Web Workers, etc.)
  • Don't need offline persistence or complex cache invalidation patterns
  • Want batteries-included (fetch, caching, retries) without config

Choose TanStack Query if you:

  • Need devtools for debugging complex cache states
  • Require offline-first with persistence
  • Have very complex cache invalidation requirements
  • Don't mind the bundle size and setup complexity

Choose SWR if you:

  • Are already in the Vercel ecosystem
  • Want something between Swing and TanStack Query
  • Need Next.js-specific optimizations

Bundle Size Comparison

@jolyui/swing/core    ~1.5KB  (fetch only, no React)
@jolyui/swing         ~3KB    (with React hooks)
swr                   ~4.5KB
@tanstack/react-query ~13KB   (+ devtools ~15KB more)

Real-World Example

Here's a complete CRUD app with Swing vs TanStack Query:

// ============ SWING ============
import { useSwing, useSwingMutation } from '@jolyui/swing';

function UserList() {
  const { data: users, isLoading } = useSwing<User[]>('/api/users');
  
  const deleteUser = useSwingMutation<void, number>(
    '/api/users',
    'DELETE',
    { invalidateOnSuccess: ['/api/users'] }
  );

  if (isLoading) return <Spinner />;
  
  return users.map(user => (
    <div key={user.id}>
      {user.name}
      <button onClick={() => deleteUser.mutate(user.id)}>Delete</button>
    </div>
  ));
}

// That's it. No providers, no configuration.

// ============ TANSTACK QUERY ============
import { QueryClient, QueryClientProvider, useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <UserList />
    </QueryClientProvider>
  );
}

function UserList() {
  const queryClient = useQueryClient();
  
  const { data: users, isLoading } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(r => r.json()),
  });
  
  const deleteUser = useMutation({
    mutationFn: (id: number) => fetch(`/api/users/${id}`, { method: 'DELETE' }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });

  if (isLoading) return <Spinner />;
  
  return users.map(user => (
    <div key={user.id}>
      {user.name}
      <button onClick={() => deleteUser.mutate(user.id)}>Delete</button>
    </div>
  ));
}

Lines of code: Swing ~15, TanStack Query ~35


Feature Comparison Table

| Feature | Swing | SWR | TanStack Query | |---------|-------|-----|----------------| | Bundle Size | ~3KB | ~4.5KB | ~13KB | | Zero Config | ✅ | ✅ | ⚠️ | | TypeScript | ✅ | ✅ | ✅ | | Suspense | ✅ | ✅ | ✅ | | SSR | ✅ | ✅ | ✅ | | Devtools | ✅ | ❌ | ✅ | | React-free Core | ✅ | ❌ | ❌ | | Built-in Fetch | ✅ | ❌ | ❌ | | Interceptors | ✅ | ❌ | ❌ | | Event System | ✅ | ❌ | ⚠️ | | Debug Mode | ✅ | ❌ | ✅ | | Automatic Retries | ✅ | ✅ | ✅ | | Retry Jitter | ✅ | ❌ | ⚠️ | | Request Deduplication | ✅ | ✅ | ✅ | | Stale-While-Revalidate | ✅ | ✅ | ✅ | | Infinite Queries | ✅ | ✅ | ✅ | | Mutations | ✅ | ⚠️ | ✅ | | Optimistic Updates | ✅ | ✅ | ✅ | | Cache Statistics | ✅ | ❌ | ⚠️ | | Cache GC | ✅ | ❌ | ✅ | | Cache Serialization | ✅ | ❌ | ✅ | | Offline Support | ⚠️ | ⚠️ | ✅ | | Persistence | ✅ | ❌ | ✅ | | Parallel Queries | ✅ | ✅ | ✅ | | Dependent Queries | ✅ | ✅ | ✅ | | Prefetching | ✅ | ✅ | ✅ | | Polling | ✅ | ✅ | ✅ | | AbortController | ✅ | ✅ | ✅ | | Memory Limits | ✅ | ❌ | ⚠️ | | Middleware System | ✅ | ❌ | ❌ | | Request Batching | ✅ | ❌ | ❌ | | API Presets | ✅ | ❌ | ❌ | | DataLoader Pattern | ✅ | ❌ | ❌ |

Advanced Features

Middleware System

Add pluggable middleware for cross-cutting concerns:

import {
  createMiddlewareManager,
  loggingMiddleware,
  authMiddleware,
  metricsMiddleware,
  getGlobalMiddleware,
} from '@jolyui/swing';

// Create middleware manager
const middleware = createMiddlewareManager();

// Add logging
middleware.use(loggingMiddleware({ logRequest: true, logResponse: true }));

// Add authentication
middleware.use(authMiddleware(() => localStorage.getItem('token'), {
  scheme: 'Bearer',
}));

// Add metrics collection
middleware.use(metricsMiddleware((metrics) => {
  analytics.track('api_request', metrics);
}));

// Chaining on client
api.use(loggingMiddleware())
   .use(authMiddleware(() => token));

// Custom middleware
middleware.use(async (context, next) => {
  console.log('Before request:', context.url);
  const response = await next();
  console.log('After request:', response.status);
  return response;
});

Built-in middleware:

  • loggingMiddleware - Log requests and responses
  • authMiddleware - Auto-inject auth headers
  • timeoutMiddleware - Set default timeouts
  • retryMiddleware - Custom retry logic
  • cacheMiddleware - Custom caching
  • transformMiddleware - Transform requests/responses
  • errorHandlerMiddleware - Global error handling
  • requestIdMiddleware - Add unique request IDs
  • metricsMiddleware - Collect performance metrics

Request Batching

Batch multiple requests for efficiency:

import { createBatcher, createDataLoader } from '@jolyui/swing';

// Simple batcher
const batcher = createBatcher({
  maxBatchSize: 10,
  batchDelay: 50,
});

// These will be batched together
const [user1, user2, user3] = await Promise.all([
  batcher.add('/api/users/1'),
  batcher.add('/api/users/2'),
  batcher.add('/api/users/3'),
]);

// DataLoader pattern (like Facebook's DataLoader)
const userLoader = createDataLoader({
  batchFn: async (ids) => {
    const response = await swing('/api/users', {
      params: { ids: ids.join(',') }
    });
    return response.data;
  },
  maxBatchSize: 100,
  cache: true,
});

// Automatically batched into single request
const [alice, bob] = await Promise.all([
  userLoader.load(1),
  userLoader.load(2),
]);

API Presets

Pre-configured clients for common API patterns:

import {
  createRESTClient,
  createGraphQLClient,
  createJSONAPIClient,
  createOAuth2Client,
} from '@jolyui/swing';

// REST API
const api = createRESTClient({
  baseURL: 'https://api.example.com',
  apiVersion: 'v1',
});
const users = await api.get('/users');

// GraphQL
const gql = createGraphQLClient({
  endpoint: 'https://api.example.com/graphql',
});
const result = await gql.query({
  query: `query GetUser($id: ID!) { user(id: $id) { name } }`,
  variables: { id: '1' },
});

// JSON:API (Drupal, Rails, etc.)
const jsonApi = createJSONAPIClient({
  baseURL: 'https://api.example.com',
  defaultInclude: ['author', 'comments'],
});

// OAuth2 Authentication
const oauth = createOAuth2Client({
  tokenEndpoint: 'https://auth.example.com/oauth/token',
  clientId: 'your-client-id',
});
const tokens = await oauth.exchangeCode(code, redirectUri);
const authApi = oauth.createAuthenticatedClient({ baseURL: 'https://api.example.com' });

Offline Support

Queue requests when offline and sync when back online:

import { createOfflineQueue, createOfflineAwareClient } from '@jolyui/swing';

// Basic offline queue
const offlineQueue = createOfflineQueue({}, {
  persist: true, // Persist to localStorage
  maxAttempts: 3,
  onSync: (req, res) => console.log('Synced:', req.url),
  onFail: (req, err) => console.error('Failed:', req.url, err),
});

// Add requests when offline
if (!offlineQueue.isOnline()) {
  offlineQueue.add('/api/comments', {
    method: 'POST',
    body: { text: 'Hello!' },
  });
}

// Start auto-sync when online
offlineQueue.start();

// Or use the offline-aware client
const { request, queue } = createOfflineAwareClient({
  baseURL: 'https://api.example.com',
});

// Automatically queues mutations when offline
await request('/api/posts', { method: 'POST', body: { title: 'New Post' } });

Better Error Messages

Get actionable error messages with hints:

import {
  isSwingError,
  getErrorHints,
  getUserFriendlyMessage,
  formatErrorForConsole,
} from '@jolyui/swing';

try {
  await swing('/api/users');
} catch (error) {
  if (isSwingError(error)) {
    // User-friendly message
    console.log(getUserFriendlyMessage(error));
    // "Unauthorized (401)"

    // Actionable hints
    console.log(getErrorHints(error));
    // ["Check authentication token", "Token may have expired", "Login again"]

    // Detailed console output
    console.log(formatErrorForConsole(error));
    // 🔴 Swing Error: HTTP_ERROR
    //    Message: Unauthorized: Invalid token
    //    Status: 401 (Unauthorized)
    //    Request: GET /api/users
    //
    //    💡 Hints:
    //       • Check authentication token
    //       • Token may have expired
    //       • Login again
  }
}

License

MIT © Johuniq