Resilience Hooks
React hooks for building fault-tolerant applications with quotas, fallbacks, and health monitoring.
useQuotaRun
Execute operations with weighted load distribution across multiple providers.
Basic Usage
import { useQuotaRun } from '@ductape/react';
function PaymentProcessor() {
const { mutate: processPayment, isLoading, data, error } = useQuotaRun({
onSuccess: (result) => {
console.log(`Payment processed by ${result.provider}`);
console.log(`Latency: ${result.latency}ms`);
alert('Payment successful!');
},
onError: (error) => {
alert(`Payment failed: ${error.message}`);
}
});
const handlePayment = (amount: number, currency: string) => {
processPayment({
tag: 'payment-quota',
input: {
amount,
currency,
customer: 'cus_123'
}
});
};
return (
<div>
<button
onClick={() => handlePayment(1000, 'usd')}
disabled={isLoading}
>
{isLoading ? 'Processing...' : 'Pay $10.00'}
</button>
{data && (
<div>
<p>Provider: {data.provider}</p>
<p>Latency: {data.latency}ms</p>
<p>Retries: {data.retriesUsed}</p>
</div>
)}
{error && <p style={{ color: 'red' }}>{error.message}</p>}
</div>
);
}
With Type Safety
interface PaymentResult {
transactionId: string;
status: 'success' | 'failed';
processorFee: number;
}
function TypeSafePayment() {
const { mutate, data } = useQuotaRun<PaymentResult>({
onSuccess: (result) => {
// TypeScript knows result.data is PaymentResult
console.log(`Transaction ID: ${result.data.transactionId}`);
console.log(`Fee: $${result.data.processorFee}`);
}
});
const handlePayment = () => {
mutate({
tag: 'payment-quota',
input: { amount: 5000, currency: 'usd' }
});
};
return (
<div>
<button onClick={handlePayment}>Process Payment</button>
{data && (
<div>
<p>Transaction: {data.data.transactionId}</p>
<p>Status: {data.data.status}</p>
</div>
)}
</div>
);
}
Email Service with Load Distribution
function EmailSender() {
const { mutate: sendEmail, isLoading } = useQuotaRun({
onSuccess: (result) => {
console.log(`Email sent via ${result.provider}`);
}
});
const handleSend = (to: string, subject: string, body: string) => {
sendEmail({
tag: 'email-quota',
input: { to, subject, body }
});
};
return (
<button
onClick={() => handleSend('user@example.com', 'Hello', 'Welcome!')}
disabled={isLoading}
>
Send Email
</button>
);
}
useFallbackRun
Execute operations with automatic failover across providers.
Basic Usage
import { useFallbackRun } from '@ductape/react';
function EmailWithFallback() {
const { mutate: sendEmail, isLoading, data, error } = useFallbackRun({
onSuccess: (result) => {
console.log(`Email sent via ${result.provider}`);
console.log(`Tried ${result.providersTried} providers`);
alert('Email sent successfully!');
},
onError: (error) => {
alert('All email providers failed');
}
});
const handleSend = () => {
sendEmail({
tag: 'email-fallback',
input: {
to: 'user@example.com',
subject: 'Important Message',
body: 'This is a critical email'
}
});
};
return (
<div>
<button onClick={handleSend} disabled={isLoading}>
{isLoading ? 'Sending...' : 'Send Email'}
</button>
{data && (
<div>
<p>✓ Sent via {data.provider}</p>
<p>Providers tried: {data.providersTried}</p>
<p>Latency: {data.latency}ms</p>
</div>
)}
{error && <p style={{ color: 'red' }}>❌ {error.message}</p>}
</div>
);
}
Payment Processing with Failover
interface PaymentResponse {
success: boolean;
transactionId: string;
gateway: string;
}
function PaymentWithFallback() {
const { mutate: processPayment, isLoading, data } = useFallbackRun<PaymentResponse>({
onSuccess: (result) => {
if (result.data.success) {
console.log(`Paid via ${result.data.gateway}`);
}
}
});
const handlePayment = (amount: number) => {
processPayment({
tag: 'payment-fallback',
input: {
amount,
currency: 'usd',
customer: 'cus_123'
}
});
};
return (
<div>
<button onClick={() => handlePayment(2500)} disabled={isLoading}>
{isLoading ? 'Processing...' : 'Pay $25.00'}
</button>
{data?.data && (
<div>
<p>Gateway: {data.data.gateway}</p>
<p>Transaction: {data.data.transactionId}</p>
<p>Providers tried: {data.providersTried}</p>
</div>
)}
</div>
);
}
useQuotaCheck
Check and consume quota with rate limiting.
Basic Quota Check
import { useQuotaCheck } from '@ductape/react';
function ApiRateLimiter() {
const { mutate: checkQuota, data, isLoading } = useQuotaCheck({
onSuccess: (result) => {
if (!result.allowed) {
alert(`Rate limited! Resets at ${new Date(result.resetsAt)}`);
}
}
});
const handleApiCall = (userId: string) => {
checkQuota({
tag: 'api-calls',
identifier: userId,
amount: 1
});
};
return (
<div>
<button
onClick={() => handleApiCall('user-123')}
disabled={isLoading}
>
Make API Call
</button>
{data && (
<div>
<p>Allowed: {data.allowed ? '✓' : '❌'}</p>
<p>Used: {data.used} / {data.limit}</p>
<p>Remaining: {data.remaining}</p>
<p>Resets: {new Date(data.resetsAt).toLocaleString()}</p>
</div>
)}
</div>
);
}
Pre-flight Quota Check
function ExpensiveOperation() {
const { mutate: checkQuota, data } = useQuotaCheck();
const { mutate: processPayment, isLoading } = useQuotaRun();
const handleSubmit = async (userId: string, amount: number) => {
// Check quota first
checkQuota(
{
tag: 'payment-quota',
identifier: userId,
amount: 1
},
{
onSuccess: (quota) => {
if (quota.allowed) {
// Proceed with payment
processPayment({
tag: 'payment-quota',
input: { amount, userId }
});
} else {
alert(`Rate limit exceeded. Try again at ${new Date(quota.resetsAt)}`);
}
}
}
);
};
return (
<button onClick={() => handleSubmit('user-123', 1000)} disabled={isLoading}>
Process Payment
</button>
);
}
useQuotaStatus
Query quota status without consuming.
Display Quota Usage
import { useQuotaStatus } from '@ductape/react';
function QuotaDisplay({ userId }: { userId: string }) {
const { mutate: getStatus, data, isLoading } = useQuotaStatus({
onSuccess: (status) => {
console.log(`User has ${status.remaining} API calls remaining`);
}
});
useEffect(() => {
getStatus({
tag: 'api-calls',
identifier: userId
});
}, [userId]);
if (isLoading) return <div>Loading...</div>;
return (
<div>
<h3>API Quota Status</h3>
{data && (
<>
<div>Used: {data.used} / {data.limit}</div>
<div>Remaining: {data.remaining}</div>
<div>Resets: {new Date(data.resetsAt).toLocaleString()}</div>
<div style={{
width: '100%',
height: '20px',
backgroundColor: '#eee'
}}>
<div style={{
width: `${(data.used / data.limit) * 100}%`,
height: '100%',
backgroundColor: data.remaining > 10 ? 'green' : 'red'
}} />
</div>
</>
)}
</div>
);
}
useQuotaSubscription
Subscribe to real-time quota changes.
Real-time Quota Monitor
import { useQuotaSubscription } from '@ductape/react';
function QuotaMonitor({ userId }: { userId: string }) {
const { data: quotaChanges, isSubscribed } = useQuotaSubscription(
{
tag: 'api-calls',
identifier: userId
},
{
onData: (events) => {
events.forEach(event => {
if (event.remaining < 10) {
alert('Warning: Low quota remaining!');
}
});
}
}
);
return (
<div>
<h3>Live Quota Monitor</h3>
<p>Status: {isSubscribed ? '🟢 Connected' : '🔴 Disconnected'}</p>
{quotaChanges && quotaChanges.length > 0 && (
<div>
<p>Last Update:</p>
<p>Remaining: {quotaChanges[0].remaining}</p>
<p>Limit: {quotaChanges[0].limit}</p>
</div>
)}
</div>
);
}
useHealthStatus
Query health status of services.
Service Health Dashboard
import { useHealthStatus } from '@ductape/react';
function HealthDashboard() {
const { mutate: checkHealth, data, isLoading } = useHealthStatus({
onSuccess: (health) => {
if (health.status === 'unhealthy') {
console.warn('Service is unhealthy!');
}
}
});
useEffect(() => {
// Check health on mount
checkHealth({ tag: 'payment-service' });
// Refresh every 30 seconds
const interval = setInterval(() => {
checkHealth({ tag: 'payment-service' });
}, 30000);
return () => clearInterval(interval);
}, []);
if (isLoading) return <div>Checking health...</div>;
return (
<div>
<h2>Service Health</h2>
{data && (
<>
<div>
Status: {' '}
<span style={{
color: data.status === 'healthy' ? 'green' :
data.status === 'degraded' ? 'orange' : 'red'
}}>
{data.status.toUpperCase()}
</span>
</div>
<div>Last Check: {data.lastCheck}</div>
<h3>Probes</h3>
<ul>
{data.probes.map(probe => (
<li key={probe.name}>
<strong>{probe.name}</strong>: {probe.status}
{probe.latency && ` (${probe.latency}ms)`}
{probe.error && ` - ${probe.error}`}
</li>
))}
</ul>
</>
)}
</div>
);
}
useHealthSubscription
Subscribe to real-time health changes.
Real-time Health Alerts
import { useHealthSubscription } from '@ductape/react';
import { useState } from 'react';
function HealthAlerts() {
const [alerts, setAlerts] = useState<string[]>([]);
const { data, isSubscribed } = useHealthSubscription(
{ tag: 'payment-service' },
{
onData: (events) => {
events.forEach(event => {
if (event.status === 'unhealthy') {
setAlerts(prev => [
...prev,
`⚠️ Service unhealthy at ${event.lastCheck}`
]);
} else if (event.status === 'healthy') {
setAlerts(prev => [
...prev,
`✓ Service recovered at ${event.lastCheck}`
]);
}
});
}
}
);
return (
<div>
<h3>Health Alerts</h3>
<p>Monitoring: {isSubscribed ? '🟢 Active' : '🔴 Inactive'}</p>
{data && data.length > 0 && (
<div>
Current Status: {data[0].status}
</div>
)}
<div>
<h4>Alert History</h4>
<ul>
{alerts.map((alert, i) => (
<li key={i}>{alert}</li>
))}
</ul>
</div>
</div>
);
}
Complete Example: Resilient Payment Flow
import { useQuotaCheck, useFallbackRun, useHealthStatus } from '@ductape/react';
import { useState } from 'react';
interface PaymentData {
amount: number;
currency: string;
userId: string;
}
function ResilientPaymentFlow() {
const [paymentData, setPaymentData] = useState<PaymentData>({
amount: 1000,
currency: 'usd',
userId: 'user-123'
});
const { mutate: checkQuota, data: quotaData } = useQuotaCheck();
const { mutate: checkHealth, data: healthData } = useHealthStatus();
const {
mutate: processPayment,
isLoading: isProcessing,
data: paymentResult
} = useFallbackRun();
const handlePayment = async () => {
// Step 1: Check quota
checkQuota(
{
tag: 'payment-quota',
identifier: paymentData.userId,
amount: 1
},
{
onSuccess: (quota) => {
if (!quota.allowed) {
alert(`Rate limit exceeded. Resets at ${new Date(quota.resetsAt)}`);
return;
}
// Step 2: Check health
checkHealth(
{ tag: 'payment-service' },
{
onSuccess: (health) => {
if (health.status === 'unhealthy') {
alert('Payment service is currently unavailable');
return;
}
// Step 3: Process payment with fallback
processPayment({
tag: 'payment-fallback',
input: paymentData
});
}
}
);
}
}
);
};
return (
<div>
<h2>Payment Processor</h2>
<div>
<label>
Amount: $
<input
type="number"
value={paymentData.amount / 100}
onChange={(e) => setPaymentData({
...paymentData,
amount: parseFloat(e.target.value) * 100
})}
/>
</label>
</div>
<button
onClick={handlePayment}
disabled={isProcessing}
>
{isProcessing ? 'Processing...' : 'Pay Now'}
</button>
{quotaData && (
<div>
<p>Quota: {quotaData.remaining} / {quotaData.limit}</p>
</div>
)}
{healthData && (
<div>
<p>Service Health: {healthData.status}</p>
</div>
)}
{paymentResult && (
<div style={{ color: 'green' }}>
✓ Payment processed via {paymentResult.provider}
<br />
Latency: {paymentResult.latency}ms
<br />
Providers tried: {paymentResult.providersTried}
</div>
)}
</div>
);
}
Best Practices
- Check Quotas First: Always check quota before expensive operations
- Monitor Health: Use health checks before critical operations
- Use Fallbacks for Critical Paths: Implement fallbacks for must-succeed operations
- Handle Loading States: Show appropriate UI during operations
- Display Error Messages: Provide clear feedback when operations fail
- Show Quota Status: Display remaining quota to users proactively
- Subscribe to Changes: Use subscriptions for real-time monitoring
- Type Your Responses: Use TypeScript generics for type-safe results