@itaylor/react-capnweb
v0.0.3
Published
React hooks and components for capnweb - A JavaScript-native RPC system, supporting multiple transport protocols
Maintainers
Readme
react-capnweb
React hooks and components for capnweb - A JavaScript-native RPC system, supporting multiple transport protocols.
Overview
react-capnweb provides React bindings for working with capnweb RPC connections
across different transport mechanisms:
- Multiple transports - WebSocket, HTTP Batch, MessagePort, or custom implementations
- Tree-shakable - Each transport in a separate module for optimal bundle size
- Consistent API - Same React hooks across all transports
- TypeScript support - Full type safety for your RPC calls
- Automatic reconnection - For WebSocket connections
- Bidirectional RPC - Server can call client methods (WebSocket, MessagePort)
Installation
Deno (JSR)
deno add @itaylor/react-capnwebNode.js (npm)
npm install @itaylor/react-capnwebUsage
Basic Setup
- Initialize your transport:
import { initCapnWebSocket } from '@itaylor/react-capnweb/websocket';
import type { MyApiInterface } from './my-api-schema';
const { useCapnWeb, useCapnWebQuery, getCapnWebStub } = initCapnWebSocket<
MyApiInterface
>('ws://localhost:8080/api');- Use the hooks in your components:
function UserProfile({ userId }: { userId: string }) {
// Simple method call with React Suspense
const userData = useCapnWeb('getUser', userId);
return (
<div>
<h1>{userData?.name}</h1>
<p>{userData?.email}</p>
</div>
);
}Complex Queries with Promise Pipelining
For complex operations with multiple API calls or promise pipelining, use
useCapnWebQuery():
function Dashboard() {
// Complex query with promise pipelining - all computations on server
const stats = useCapnWebQuery('dashboardStats', (api) => {
const userData = api.getUser('user-123');
const userPosts = api.getUserPosts(userData.id);
return api.calculateStats(userPosts);
}, []);
return <div>Total: {stats?.total}</div>;
}Direct API Access
For imperative control, use getCapnWebStub():
function UserManager() {
const api = getCapnWebStub();
const [users, setUsers] = useState([]);
async function loadUsers() {
const data = await api.listUsers();
setUsers(data);
}
async function createUser(name: string, email: string) {
await api.createUser({ name, email });
loadUsers();
}
useEffect(() => {
loadUsers();
}, []);
return (
<div>
<button onClick={() => createUser('Alice', '[email protected]')}>
Add User
</button>
<ul>
{users.map((user) => <li key={user.id}>{user.name}</li>)}
</ul>
</div>
);
}Bidirectional RPC (WebSocket / MessagePort)
The server can call methods on your client:
import { RpcTarget } from 'capnweb';
class ClientApi extends RpcTarget {
showNotification(message: string) {
toast.show(message);
}
updateProgress(percent: number) {
progressBar.update(percent);
}
}
initCapnWebSocket<ServerApi>(
'ws://localhost:8080',
{
localMain: new ClientApi(), // Server can call these methods
},
);Transport Options
WebSocket
Persistent bidirectional connection for real-time communication.
Best for:
- Real-time applications
- Long-lived connections
- Server-initiated updates
- Low-latency requirements
import { initCapnWebSocket } from '@itaylor/react-capnweb/websocket';
const {
useCapnWeb,
useCapnWebQuery,
getCapnWebStub,
close,
useConnectionState,
} = initCapnWebSocket<MyApi>(
'ws://localhost:8080/api',
{
timeout: 5000, // Connection timeout in ms
retries: 10, // Max reconnection attempts
backoffStrategy: (retryCount) => retryCount * 1000, // Optional: custom backoff
localMain: new MyClientApi(), // Optional: expose API to server
onConnected: () => console.log('Connected!'),
onDisconnected: (reason) => console.log('Disconnected:', reason),
onReconnecting: (attempt) => console.log('Reconnecting, attempt', attempt),
onReconnectFailed: () => console.log('All retries exhausted'),
},
);
// The WebSocket connection persists across provider mount/unmount.
// To manually close the connection when needed:
// close();
// Track connection state in your components:
function ConnectionStatus() {
const state = useConnectionState();
if (state.status === 'connecting') {
return <div>Connecting...</div>;
}
if (state.status === 'reconnecting') {
return <div>Reconnecting (attempt {state.attempt})...</div>;
}
if (state.status === 'connected') {
return <div>Connected</div>;
}
return <div>Disconnected</div>;
}HTTP Batch
Stateless HTTP requests for serverless and edge deployments.
Best for:
- Serverless/edge functions
- CDN-friendly applications
- Simple request/response patterns
- Better proxy/load balancer compatibility
Note: HTTP Batch sessions are single-use per batch. Each call to
useCapnWeb(), useCapnWebQuery(), or getCapnWebStub() creates a new batch
session. To batch multiple calls together, get the api once and make all calls
before awaiting any of them.
Important: getCapnWebStub() is not actually a React hook - it doesn't use
context or state, just creates a fresh session. This means you can call it
anywhere, including inside async functions and event handlers.
import { initCapnHttpBatch } from '@itaylor/react-capnweb/http-batch';
const { useCapnWeb, useCapnWebQuery, getCapnWebStub } = initCapnHttpBatch<
MyApi
>(
'/api/rpc',
{
headers: { 'Authorization': 'Bearer token123' },
credentials: 'include',
},
);
function MyComponent() {
// ✅ Single batch - all calls in one HTTP request
const [user, posts, comments] = useCapnWebQuery(
'userPostsComments',
(api) => {
const userPromise = api.getUser('123');
const postsPromise = api.getUserPosts('123');
const commentsPromise = api.getUserComments('123');
return Promise.all([userPromise, postsPromise, commentsPromise]);
},
['123'],
);
// ✅ Or call getCapnWebStub in async handlers (NOT a real React hook!)
const handleAction = async () => {
// Each call to getCapnWebStub() creates a new session
const result1 = await getCapnWebStub().getUser('123'); // Batch 1
const result2 = await getCapnWebStub().getPosts('123'); // Batch 2
// To batch multiple calls together, get api once before awaiting:
const api = getCapnWebStub();
const p1 = api.getUser('123');
const p2 = api.getComments('123');
const [user, comments] = await Promise.all([p1, p2]); // Single batch
};
}MessagePort
Communication with Web Workers, iframes, and Service Workers.
Best for:
- Web Worker communication
- iframe messaging
- Service Worker integration
- Isolated JavaScript contexts
import { initCapnMessagePort } from '@itaylor/react-capnweb/message-port';
const channel = new MessageChannel();
const { useCapnWeb, useCapnWebQuery, getCapnWebStub, close } = initCapnMessagePort<MyApi>(
WorkerApi
>(
channel.port1,
{
localMain: new ParentApi(), // Optional: expose API to worker
},
);
// The MessagePort connection persists across provider mount/unmount.
// To manually close the connection when needed:
// close();
// Send port2 to worker
worker.postMessage({ port: channel.port2 }, [channel.port2]);Custom Transport
Implement your own transport for custom protocols.
Best for:
- Custom networking protocols
- Testing with mock transports
- Adding middleware (logging, encryption)
- Specialized communication channels
import { initCapnCustomTransport } from '@itaylor/react-capnweb/custom-transport';
import type { RpcTransport } from 'capnweb';
class MyTransport implements RpcTransport {
async send(message: string): Promise<void> {/* ... */}
async receive(): Promise<string> {/* ... */}
abort?(reason: any): void {/* ... */}
}
const { useCapnWeb, useCapnWebQuery, getCapnWebStub, close } =
initCapnCustomTransport<
MyApi
>(new MyTransport(), {
localMain: new MyLocalApi(),
});
// The transport connection persists across provider mount/unmount.
// To manually close the connection when needed:
// close();API Reference
Common Interface
All transport initialization functions return the same CapnWebHooks<T>
interface:
interface CapnWebHooks<T> {
useCapnWeb<K extends keyof T>(
apiName: K,
...args: Parameters<T[K]>
): Awaited<ReturnType<T[K]>>;
useCapnWebQuery<R>(
operationName: string,
fn: (api: RpcApi<T>) => Promise<R>,
...deps: any[]
): R;
getCapnWebStub: () => RpcApi<T>;
close: () => void; // Manually close the connection and dispose the session
}Note: All transports include a close() function for manual resource
cleanup:
- WebSocket: Closes the connection and prevents reconnection
- MessagePort: Closes the port and disposes the session
- Custom Transport: Calls
abort()on the transport if available and disposes the session - HTTP Batch: No
close()function; sessions are automatically cleaned up after each batch
useCapnWeb<K>(apiName, ...args)
Hook for simple RPC method calls with React Suspense support. Automatically caches calls based on method name and arguments.
Parameters:
apiName: Name of the API method to call...args: Arguments to pass to the method
Returns: The resolved value from the RPC call
Example:
const user = useCapnWeb('getUser', userId);
const sum = useCapnWeb('add', 5, 3);useCapnWebQuery<R>(operationName, fn, ...deps)
Hook for complex RPC queries with React Suspense support. Use this for operations involving multiple API calls, promise pipelining, or custom logic.
Parameters:
operationName: Unique name for this operation (used for promise caching for compatibility with React Suspense)fn: Function that takes the API and returns a Promise...deps: Dependencies that affect the query
Returns: The resolved value from the RPC call
Example:
const stats = useCapnWebQuery('dashboardStats', (api) => {
const userData = api.getUser('user-123');
const userPosts = api.getUserPosts(userData.id);
return api.calculateStats(userPosts);
}, [userId]);getCapnWebStub()
Gets direct access to the RPC API stub. Use this when you need imperative control over RPC calls (e.g., in event handlers, effects, or callbacks). As this is not a hook, it does not support suspense.
Returns: The RPC API stub
Example:
const api = getCapnWebStub();
async function handleClick() {
const result = await api.someMethod();
console.log(result);
}Transport-Specific Options
WebSocket Options
interface WebSocketOptions {
timeout?: number; // Connection timeout in ms (default: 5000)
retries?: number; // Max reconnection attempts (default: 10)
backoffStrategy?: (retryCount: number) => number; // Delay calculation function
localMain?: any; // Local API for bidirectional RPC
sessionOptions?: RpcSessionOptions; // Additional capnweb options
onConnected?: () => void; // Callback when connection established
onDisconnected?: (reason?: string) => void; // Callback when connection lost
onReconnecting?: (attempt: number) => void; // Callback when reconnection starts
onReconnectFailed?: () => void; // Callback when all retries exhausted
}
// Returns:
interface WebSocketCapnWebHooks<T> extends CapnWebHooks<T> {
useConnectionState: () => WebSocketConnectionState;
}
type WebSocketConnectionState =
| { status: 'connecting'; attempt: number }
| { status: 'connected' }
| { status: 'reconnecting'; attempt: number; nextRetryMs?: number }
| { status: 'disconnected'; reason?: string }
| { status: 'closed' };
// Connection Lifecycle:
// - WebSocket connection persists across provider mount/unmount
// - Only closes when disconnected, retries exhausted, or close() is calledBackoff Strategy:
The backoffStrategy function controls the delay before each reconnection
attempt. It receives the current retry count (1-indexed) and returns the delay
in milliseconds.
Default: Exponential backoff with jitter
- Base delay: 1s, doubles each retry (1s, 2s, 4s, 8s, 16s, max 30s)
- Adds random jitter of 0-1000ms to prevent thundering herd
// Custom fixed delay of 5 seconds
backoffStrategy: (() => 5000);
// Linear backoff: 2s, 4s, 6s, 8s...
backoffStrategy: ((retryCount) => retryCount * 2000);
// Custom exponential without jitter, max 60s
backoffStrategy: ((retryCount) =>
Math.min(1000 * Math.pow(2, retryCount - 1), 60000));Connection State Hook:
The useConnectionState() hook returns the current connection state, allowing
you to display connection status in your UI.
function ConnectionIndicator() {
const state = useConnectionState();
switch (state.status) {
case 'connecting':
return <Spinner>Connecting...</Spinner>;
case 'connected':
return <Badge color='green'>Connected</Badge>;
case 'reconnecting':
return (
<Banner>
Reconnecting... (attempt {state.attempt})
{state.nextRetryMs &&
` Next retry in ${Math.round(state.nextRetryMs / 1000)}s`}
</Banner>
);
case 'disconnected':
return <Alert>Disconnected{state.reason && `: ${state.reason}`}</Alert>;
case 'closed':
return <Badge color='gray'>Connection closed</Badge>;
}
}Connection Callbacks:
Use callbacks for side effects like logging, analytics, or notifications:
initCapnWebSocket<MyApi>('ws://localhost:8080', {
onConnected: () => {
console.log('WebSocket connected');
analytics.track('ws_connected');
},
onDisconnected: (reason) => {
console.warn('WebSocket disconnected:', reason);
},
onReconnecting: (attempt) => {
toast.info(`Reconnecting (attempt ${attempt})...`);
},
onReconnectFailed: () => {
toast.error('Unable to reconnect. Please refresh the page.');
},
});HTTP Batch Options
interface HttpBatchOptions {
headers?: Record<string, string>; // Custom request headers
credentials?: RequestCredentials; // 'include', 'same-origin', etc.
mode?: RequestMode; // 'cors', 'no-cors', etc.
cache?: RequestCache; // Cache mode
redirect?: RequestRedirect; // Redirect handling
referrerPolicy?: ReferrerPolicy; // Referrer policy
sessionOptions?: RpcSessionOptions; // Additional capnweb options
onError?: (error: Error) => void; // Error handler
}HTTP Batch behavioral notes:
HTTP Batch uses the same API as other transports, but has different session lifecycle behavior because capnweb HTTP Batch sessions are single-use per batch:
- Each call to
useCapnWeb(),useCapnWebQuery(), orgetCapnWebStub()creates a new batch session - To batch multiple calls together, get the api once and make all calls before awaiting any of them
- Don't await inside the
useCapnWebQuery()callback - the batch ends when you await
Batching examples:
// ✅ Single batch - get api once, make calls, then await
const api = getCapnWebStub();
const p1 = api.call1();
const p2 = api.call2();
const [r1, r2] = await Promise.all([p1, p2]); // Batch sent here
// ✅ Single batch with useCapnWebQuery
const result = useCapnWebQuery('myBatch', (api) => {
const p1 = api.call1();
const p2 = api.call2();
return Promise.all([p1, p2]);
});
// ✅ Promise pipelining (single batch)
const result = useCapnWeb((api) => {
const userId = api.lookupUser('alice');
return api.getUserData(userId); // userId resolved on server
});
// ✅ Multiple batches - call getCapnWebStub() fresh each time (NOT a real hook!)
const r1 = await getCapnWebStub().call1(); // HTTP request 1
const r2 = await getCapnWebStub().call2(); // HTTP request 2
// ❌ Won't work - awaiting inside useCapnWebQuery ends the batch
const result = useCapnWebQuery('myQuery', async (api) => {
const r1 = await api.call1(); // Batch ends here
const r2 = await api.call2(); // Session already closed!
return [r1, r2];
});MessagePort Options
interface MessagePortOptions {
localMain?: any; // Local API for bidirectional RPC
sessionOptions?: RpcSessionOptions; // Additional capnweb options
onDisconnect?: () => void; // Disconnect callback (limited browser support)
}
// Note: onDisconnect uses the 'close' event which has limited browser support.
// Connection persists across provider mount/unmount.Custom Transport Options
interface CustomTransportOptions {
localMain?: any; // Local API for bidirectional RPC
sessionOptions?: RpcSessionOptions; // Additional capnweb options
onError?: (error: Error) => void; // Error handler
}
// Connection persists across provider mount/unmount.Features
Type Safety
WebSocket Connection Lifecycle
The WebSocket connection persists across provider mount/unmount cycles. This means:
- Connection is created once when initCapnWebSocket is called
- Connection only closes when it disconnects/errors and exhausts retries, or
when you call
close()
const { close } = initCapnWebSocket<MyApi>(
'ws://localhost:8080/api',
);
function App() {
useEffect(() => {
// Cleanup function to close connection when app unmounts
return () => close();
}, []);
return <YourApp />;
}This design is efficient for app-level WebSocket connections that should survive navigation and component remounting.
Type Safety
All RPC calls are fully typed based on your Cap'n Web schema:
interface MyApi extends RpcTarget {
getUser(id: string): Promise<User>;
listUsers(): Promise<User[]>;
}
// TypeScript knows the return types!
const user = useCapnWebQuery((api) => api.getUser('123'), ['123']);
// user is typed as User | undefinedReact Suspense Support
The useCapnWeb and useCapnWebQuery hooks integrate with React Suspense
boundaries:
<Suspense fallback={<Loading />}>
<UserProfile userId='123' />
</Suspense>;Tree-Shaking
Import only the transport you need for optimal bundle size:
// Only includes WebSocket transport code
import { initCapnWebSocket } from '@itaylor/react-capnweb/websocket';
// Only includes HTTP Batch transport code
import { initCapnHttpBatch } from '@itaylor/react-capnweb/http-batch';Comparison of Transports
| Feature | WebSocket | HTTP Batch | MessagePort | Custom | | ---------------------- | ---------- | ------------- | ----------- | ------- | | Persistent connection | ✅ | ❌ | ✅ | Depends | | Automatic reconnection | ✅ | N/A | ❌ | Custom | | Bidirectional RPC | ✅ | ❌ | ✅ | Depends | | Server push | ✅ | ❌ | ✅ | Depends | | Serverless friendly | ❌ | ✅ | N/A | Depends | | CDN compatible | ❌ | ✅ | N/A | Depends | | Worker/iframe support | ❌ | ❌ | ✅ | Depends | | Latency | Low | Medium | Very Low | Depends | | Session lifecycle | Long-lived | Single-use⁽¹⁾ | Long-lived | Depends |
⁽¹⁾ Note: HTTP Batch sessions are single-use per batch. Each call to
useCapnWeb(), useCapnWebQuery(), or method call via getCapnWebStub()
creates a new session. To batch multiple calls together, make all calls before
awaiting any of them.
Examples
Switching Transports
The beauty of the consistent API is you can switch transports without changing your component code:
// Development: Use WebSocket for hot reload friendly connection
const { useCapnWeb, useCapnWebQuery, getCapnWebStub } = import.meta.env.DEV
? initCapnWebSocket('ws://localhost:8080')
: initCapnHttpBatch('/api/rpc');
// Note: HTTP Batch has single-use sessions, so you may need to adjust
// batching strategy, but the API is the sameMulti-Transport Application
Use different transports for different purposes:
// Main API via HTTP Batch for serverless
const httpApi = initCapnHttpBatch<MainApi>('/api/rpc');
// Real-time updates via WebSocket
const wsApi = initCapnWebSocket<RealtimeApi>('ws://localhost:8080/live');
function App() {
return <YourApp />;
}
function YourApp() {
// Both use the same API!
const httpData = httpApi.getCapnWebStub();
const wsData = wsApi.getCapnWebStub();
// HTTP Batch: batch calls together by not awaiting immediately
const loadData = async () => {
const p1 = httpData.getUser();
const p2 = httpData.getSettings();
const [user, settings] = await Promise.all([p1, p2]); // Single batch
};
// WebSocket: call anytime
const subscribe = () => wsData.subscribe('updates');
}Requirements
- React 19+ (requires the
usehook) - capnweb 0.3.0+
License
MIT License - see LICENSE for details.
Related Projects
- capnweb - JavaScript Cap'n Proto RPC
- type-router-react - Type-safe routing for React
