@jolyui/swing
v1.0.0
Published
Lightweight, powerful data fetching for React. Simple by design, tiny by nature.
Downloads
10
Maintainers
Readme
@jolyui/swing
Lightweight, powerful data fetching for React
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/swingQuick 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 responsesauthMiddleware- Auto-inject auth headerstimeoutMiddleware- Set default timeoutsretryMiddleware- Custom retry logiccacheMiddleware- Custom cachingtransformMiddleware- Transform requests/responseserrorHandlerMiddleware- Global error handlingrequestIdMiddleware- Add unique request IDsmetricsMiddleware- 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
