React Hooks Documentation
React hooks for Count Cachula - stale-while-revalidate caching with Suspense support.
Installation
npm install @countcachula/react @countcachula/coreBasic Setup with Suspense
import { Suspense } from 'react';import { useFetch } from '@countcachula/react';
function IssueList() { // This suspends until data is available const issues = useFetch<Issue[]>('/api/issues');
// Component only renders when data exists - no loading state needed! return ( <ul> {issues.map(issue => ( <li key={issue.id}>{issue.title}</li> ))} </ul> );}
function App() { return ( <ErrorBoundary fallback={<ErrorMessage />}> <Suspense fallback={<LoadingSpinner />}> <IssueList /> </Suspense> </ErrorBoundary> );}Core Concepts
Suspense Integration
Count Cachula's React hooks are built around React's Suspense pattern:
- • No Loading States - Suspense handles loading UI automatically
- • Type Safety - Data is always the correct type, never undefined
- • Clean Components - No conditional rendering for loading states
- • Error Boundaries - Errors throw to nearest Error Boundary
Suspense Behavior:
- 1. First render (no cache) - Suspends until data arrives
- 2. First render (with cache) - Returns cached data immediately, fetches fresh data in background
- 3. Fresh data arrives - Component re-renders with new data, no loading state
- 4. Errors - Throws to nearest Error Boundary
Module-Level Caching
The React package leverages module-level caching for optimal performance:
// All these components share the same cachefunction ComponentA() { const data = useFetch('/api/data'); // First request}
function ComponentB() { const data = useFetch('/api/data'); // Returns cached instantly}
function ComponentC() { const data = useFetch('/api/data'); // Also cached}Benefits: No prop drilling, automatic deduplication, shared state across components
Real-time Updates via SSE
Connect to server-sent events for automatic cache invalidation:
function App() { // Establish SSE connection for cache invalidation useConnection('/api/cache-events', { preload: { onHint: true, maxConcurrent: 3, }, });
return <YourApp />;}When server sends invalidation events:
- • Affected components automatically re-render with fresh data
- • No manual cache management needed
- • Preload hints warm cache for likely next navigation
API Reference
useFetch<T>(url, options?): T
Fetches data with automatic caching and Suspense support.
Parameters:
url: string- The URL to fetchoptions?: FetchOptions- Optional configuration:- •
headers- Request headers - •
transform- Custom transform function (defaults to JSON parsing) - • All other standard
RequestInitoptions exceptmethod
- •
Returns:
T- The fetched and typed data. Never returnsundefinedor loading states.
Example:
function UserProfile({ userId }: { userId: string }) { const user = useFetch<User>(`/api/users/${userId}`, { headers: { Authorization: `Bearer ${token}`, }, });
return <div>Welcome, {user.name}!</div>;} useConnection(endpoint, options?): void
Manages SSE connection for real-time cache invalidation.
Parameters:
endpoint: string- SSE endpoint URLoptions?: ConnectionOptions- Connection configuration:- •
preload- Preloading configuration- -
onInvalidate- Preload on invalidation events - -
onHint- Preload on hint events - -
maxConcurrent- Max concurrent preloads
- -
- •
Example:
function App() { useConnection('/api/cache-events', { preload: { onInvalidate: true, // Preload on cache invalidation onHint: true, // Preload on server hints maxConcurrent: 5, // Limit concurrent preloads }, });
return <Router />;}Guides
Basic Data Fetching
Simple List Component
interface Issue { id: number; title: string; status: 'open' | 'closed';}
function IssueList() { const issues = useFetch<Issue[]>('/api/issues');
return ( <div> <h1>Issues ({issues.length})</h1> <ul> {issues.map(issue => ( <li key={issue.id}> <span className={issue.status}>{issue.title}</span> </li> ))} </ul> </div> );}Detail Component with Parameters
function IssueDetail({ id }: { id: string }) { const issue = useFetch<Issue>(`/api/issues/${id}`); const comments = useFetch<Comment[]>(`/api/issues/${id}/comments`);
return ( <div> <h1>{issue.title}</h1> <p>Status: {issue.status}</p>
<h2>Comments ({comments.length})</h2> {comments.map(comment => ( <div key={comment.id}> <strong>{comment.author}</strong> <p>{comment.body}</p> </div> ))} </div> );}Custom Transform Function
function ReadmeViewer() { const content = useFetch<string>('/api/readme.txt', { transform: async (response) => response.text(), });
return <pre>{content}</pre>;}
function ImageViewer() { const imageBlob = useFetch<string>('/api/chart.png', { transform: async (response) => { const blob = await response.blob(); return URL.createObjectURL(blob); }, });
return <img src={imageBlob} alt="Chart" />;}Authentication Patterns
Bearer Token Authentication
function useAuthenticatedFetch<T>(url: string) { const token = useAuthToken(); // Your auth hook
return useFetch<T>(url, { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, });}
function ProtectedData() { const userData = useAuthenticatedFetch<UserData>('/api/user');
return <div>Welcome, {userData.name}!</div>;}SSE Connection Management
App-Level Connection
function App() { // Establish connection at app root useConnection('/api/cache-events', { preload: { onHint: true, maxConcurrent: 3, }, });
return ( <Router> <Routes> <Route path="/" element={<Home />} /> <Route path="/issues" element={<IssueList />} /> <Route path="/issues/:id" element={<IssueDetail />} /> </Routes> </Router> );}Conditional Connections
function App() { const { user } = useAuth();
// Only connect if authenticated useConnection(user ? '/api/cache-events' : null, { preload: { onInvalidate: true, onHint: true, maxConcurrent: 5, }, });
return user ? <AuthenticatedApp /> : <LoginForm />;}Custom Connection Handling
function useSmartConnection() { const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => { const handleOnline = () => setIsOnline(true); const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline);
return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, []);
// Only connect when online useConnection(isOnline ? '/api/cache-events' : null);}
function App() { useSmartConnection(); return <YourApp />;}Examples
Complete App Setup
import { Suspense } from 'react';import { BrowserRouter, Routes, Route } from 'react-router-dom';import { ErrorBoundary } from 'react-error-boundary';import { useConnection } from '@countcachula/react';
function ErrorFallback({ error, resetErrorBoundary }) { return ( <div className="error"> <h2>Oops! Something went wrong</h2> <p>{error.message}</p> <button onClick={resetErrorBoundary}>Try again</button> </div> );}
function LoadingSpinner() { return ( <div className="spinner"> <div className="loading-animation" /> <p>Loading...</p> </div> );}
function App() { // Connect to SSE for real-time updates useConnection('/api/cache-events', { preload: { onHint: true, onInvalidate: false, maxConcurrent: 3, }, });
return ( <BrowserRouter> <ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => window.location.reload()} > <div className="app"> <nav> <Link to="/">Home</Link> <Link to="/issues">Issues</Link> </nav>
<main> <Suspense fallback={<LoadingSpinner />}> <Routes> <Route path="/" element={<HomePage />} /> <Route path="/issues" element={<IssuesPage />} /> <Route path="/issues/:id" element={<IssueDetailPage />} /> </Routes> </Suspense> </main> </div> </ErrorBoundary> </BrowserRouter> );}Master-Detail Pattern
interface Issue { id: number; title: string; status: 'open' | 'closed'; labels: string[];}
function IssuesPage() { return ( <div className="issues-layout"> <aside className="sidebar"> <Suspense fallback={<div>Loading issues...</div>}> <IssueList /> </Suspense> </aside>
<main className="content"> <Suspense fallback={<div>Loading issue details...</div>}> <IssueDetail /> </Suspense> </main> </div> );}
function IssueList() { const issues = useFetch<Issue[]>('/api/issues'); const [selectedId, setSelectedId] = useSearchParams();
return ( <div> <h2>Issues ({issues.length})</h2> <ul> {issues.map(issue => ( <li key={issue.id} className={selectedId === issue.id.toString() ? 'selected' : ''} onClick={() => setSelectedId({ id: issue.id.toString() })} > <span className={issue.status}>{issue.title}</span> <div className="labels"> {issue.labels.map(label => ( <span key={label} className="label">{label}</span> ))} </div> </li> ))} </ul> </div> );}
function IssueDetail() { const [searchParams] = useSearchParams(); const issueId = searchParams.get('id');
if (!issueId) { return <div>Select an issue to view details</div>; }
const issue = useFetch<Issue>(`/api/issues/${issueId}`); const comments = useFetch<Comment[]>(`/api/issues/${issueId}/comments`);
return ( <div> <h1>{issue.title}</h1> <p>Status: <span className={issue.status}>{issue.status}</span></p>
<div className="comments"> <h3>Comments ({comments.length})</h3> {comments.map(comment => ( <div key={comment.id} className="comment"> <strong>{comment.author}</strong> <time>{new Date(comment.createdAt).toLocaleString()}</time> <p>{comment.body}</p> </div> ))} </div> </div> );}Key Benefits
No loading states
Suspense handles loading UI automatically
Type safety
Data is always the correct type, never undefined
Clean components
No conditional rendering for loading/error states
Stale-while-revalidate
Instant cache hits with background updates
Real-time updates
SSE integration for cache invalidation
React 18+ required
Built for modern React with Suspense