React Hooks Documentation

React hooks for Count Cachula - stale-while-revalidate caching with Suspense support.

Installation

Terminal window
npm install @countcachula/react @countcachula/core

Basic 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. 1. First render (no cache) - Suspends until data arrives
  2. 2. First render (with cache) - Returns cached data immediately, fetches fresh data in background
  3. 3. Fresh data arrives - Component re-renders with new data, no loading state
  4. 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 cache
function 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 fetch
  • options?: FetchOptions - Optional configuration:
    • headers - Request headers
    • transform - Custom transform function (defaults to JSON parsing)
    • • All other standard RequestInit options except method

Returns:

  • T - The fetched and typed data. Never returns undefined or 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 URL
  • options?: 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

Next Steps