⚡ Async State – Managing Asynchronous Data
Stunk's asyncChunk
eliminates the boilerplate of async state management by automatically handling loading states, errors, retries, and caching. Built for modern applications that need robust data fetching.
🚀 Key Features
✅ Automatic State Management → Loading, error, and data states handled automatically
✅ Smart Caching & Refresh → Configurable stale time and cache invalidation
✅ Retry Logic → Built-in retry with exponential backoff
✅ Optimistic Updates → Mutate data optimistically with rollback support
✅ Type-Safe → Full TypeScript support with proper error typing
✅ Framework Agnostic → Works everywhere, React hooks included
🔗 Basic Usage
Simple Data Fetching
typescript
import { asyncChunk } from "stunk";
type User = {
id: number;
name: string;
email: string;
};
// Basic async chunk
const userChunk = asyncChunk<User>(async () => {
const response = await fetch("/api/user");
if (!response.ok) throw new Error("Failed to fetch user");
return response.json();
});
// Subscribe to state changes
userChunk.subscribe(({ loading, error, data }) => {
if (loading) console.log("Loading user...");
if (error) console.log("Error:", error.message);
if (data) console.log("User loaded:", data.name);
});
Parameterized Fetching
typescript
// Async chunk with parameters
const userChunk = asyncChunk<User, Error, [number]>(
async (userId: number) => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error("User not found");
return response.json();
}
);
// Load specific user
await userChunk.reload(123);
// Or set params for future calls
userChunk.setParams(123);
⚙️ Advanced Configuration
Caching and Refresh Strategies
typescript
const userChunk = asyncChunk<User>(fetchUser, {
refresh: {
staleTime: 60000, // Data fresh for 1 minute
cacheTime: 300000, // Cache for 5 minutes
refetchInterval: 30000 // Auto-refresh every 30 seconds
},
retryCount: 3, // Retry failed requests 3 times
retryDelay: 1000, // Wait 1 second between retries
enabled: true, // Start fetching immediately
});
Conditional Fetching
typescript
const userChunk = asyncChunk<User>(fetchUser, {
enabled: false // Don't fetch on creation
});
// Enable fetching when needed
userChunk.reload(); // Manually trigger fetch
// Or with parameters
const postChunk = asyncChunk<Post[], Error, [string]>(
async (userId: string) => {
if (!userId) throw new Error("User ID required");
return fetchUserPosts(userId);
},
{ enabled: false }
);
// Only fetch when we have a valid user ID
if (currentUserId) {
postChunk.reload(currentUserId);
}
🔄 Data Operations
Manual Reloading
typescript
// Force reload (ignores cache)
await userChunk.reload();
// Smart refresh (respects stale time)
await userChunk.refresh();
// With parameters
await userChunk.reload(newUserId);
Optimistic Updates
typescript
// Optimistically update user data
userChunk.mutate((currentUser) => {
if (!currentUser) return { id: 0, name: "New User", email: "" };
return {
...currentUser,
name: "Updated Name"
};
});
// The mutation is type-safe - TypeScript will catch errors
userChunk.mutate((user) => ({
...user,
invalidProperty: "value" // ❌ TypeScript Error
}));
State Management
typescript
// Reset to initial state
userChunk.reset();
// Destroy the chunk completely
userChunk.destroy();
Performance Optimization
typescript
// Use stale-while-revalidate pattern
const dataChunk = asyncChunk(fetchData, {
refresh: {
staleTime: 30000, // Consider data stale after 30 seconds
cacheTime: 600000, // Keep in cache for 10 minutes
}
});
// Preload data
dataChunk.refresh(); // Load in background
// Use optimistic updates for better UX
function updateUser(updates: Partial<User>) {
// Update UI immediately
userChunk.mutate(user => ({ ...user, ...updates }));
// Send to server
updateUserAPI(updates).catch(() => {
// Revert on error
userChunk.reload();
});
}
🔧 API Reference
AsyncChunk Interface
typescript
interface AsyncChunk<T, E extends Error = Error> {
// Core chunk methods
get(): AsyncState<T, E>;
set(state: AsyncState<T, E>): void;
subscribe(callback: (state: AsyncState<T, E>) => void): () => void;
destroy(): void;
// Async-specific methods
reload(...params: P): Promise<void>; // Force reload
refresh(...params: P): Promise<void>; // Smart refresh
mutate(mutator: (data: T | null) => T): void; // Optimistic update
reset(): void; // Reset to initial state
cleanup(): void; // Clean up timers
setParams(...params: P): void; // Set parameters
}
interface AsyncState<T, E extends Error> {
loading: boolean;
error: E | null;
data: T | null;
lastFetched?: number;
}
Next: Before we learn how to merge multiple async states efficiently, let me introduce the once
utility function. 🚀