🎯 Production Example: Live Streaming Dashboard
See React integration in production! Our live dashboard is built with React, TypeScript, and uses the patterns described in this guide. Streams 6 cryptocurrencies using POST /stream with custom hooks, real-time UI updates, and proper error handling. Open the dashboard and inspect the Network tab to see SSE in action! View Source →
Integration overview
Learn how to integrate DexPaprika’s streaming API into React and Next.js applications with:- Custom React hooks for streaming
- TypeScript support
- Server-side rendering considerations
- Production-ready error handling
Finding Token Addresses
Before streaming, you need token addresses. Use the REST API:Search Tokens
Use the Search API to find tokens by name or symbol
Get Networks
Use the Networks API to get supported chains
Copy
# Search for USDC tokens
curl "https://api.dexpaprika.com/search?query=USDC"
# Get all supported networks
curl "https://api.dexpaprika.com/networks"
Quick Example
Copy
import { useEffect, useState } from 'react';
function useCryptoPrice(chain: string, address: string) {
const [price, setPrice] = useState<number | null>(null);
const [status, setStatus] = useState<'connecting' | 'connected' | 'error'>('connecting');
useEffect(() => {
const url = new URL('https://streaming.dexpaprika.com/stream');
url.searchParams.set('method', 't_p');
url.searchParams.set('chain', chain);
url.searchParams.set('address', address);
const eventSource = new EventSource(url.toString());
eventSource.onopen = () => setStatus('connected');
eventSource.addEventListener('t_p', (event) => {
const data = JSON.parse(event.data);
setPrice(parseFloat(data.p));
});
eventSource.onerror = () => setStatus('error');
return () => eventSource.close();
}, [chain, address]);
return { price, status };
}
// Usage
function PriceDisplay() {
const { price, status } = useCryptoPrice('ethereum', '0xc02aa...');
return (
<div>
{status === 'connected' && price && (
<span>${price.toFixed(2)}</span>
)}
</div>
);
}
Complete Implementation
1. Install Dependencies
Copy
npm install --save-dev @types/react
2. Create the Streaming Hook
Createhooks/useCryptoStream.ts:
Show View complete hook implementation
Show View complete hook implementation
Copy
import { useEffect, useRef, useState, useCallback } from 'react';
interface PriceData {
a: string; // address
c: string; // chain
p: string; // price
t: number; // timestamp
}
interface StreamOptions {
chain: string;
address: string;
onPrice?: (price: number) => void;
reconnect?: boolean;
reconnectDelay?: number;
maxReconnectAttempts?: number;
}
interface StreamState {
price: number | null;
status: 'idle' | 'connecting' | 'connected' | 'error' | 'disconnected';
error: Error | null;
lastUpdate: Date | null;
}
export function useCryptoStream({
chain,
address,
onPrice,
reconnect = true,
reconnectDelay = 1000,
maxReconnectAttempts = 5,
}: StreamOptions): StreamState & { reconnect: () => void; disconnect: () => void } {
const [state, setState] = useState<StreamState>({
price: null,
status: 'idle',
error: null,
lastUpdate: null,
});
const eventSourceRef = useRef<EventSource | null>(null);
const reconnectAttemptsRef = useRef(0);
const reconnectTimeoutRef = useRef<NodeJS.Timeout>();
const connect = useCallback(() => {
// Clean up any existing connection
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
setState(prev => ({ ...prev, status: 'connecting', error: null }));
const url = new URL('https://streaming.dexpaprika.com/stream');
url.searchParams.set('method', 't_p');
url.searchParams.set('chain', chain);
url.searchParams.set('address', address);
const eventSource = new EventSource(url.toString());
eventSourceRef.current = eventSource;
eventSource.onopen = () => {
setState(prev => ({ ...prev, status: 'connected' }));
reconnectAttemptsRef.current = 0;
};
eventSource.addEventListener('t_p', (event: MessageEvent) => {
try {
const data: PriceData = JSON.parse(event.data);
const price = parseFloat(data.p);
setState(prev => ({
...prev,
price,
lastUpdate: new Date(data.t * 1000),
}));
onPrice?.(price);
} catch (error) {
console.error('Failed to parse price data:', error);
}
});
eventSource.onerror = () => {
setState(prev => ({
...prev,
status: 'error',
error: new Error('Connection lost'),
}));
eventSource.close();
eventSourceRef.current = null;
// Handle reconnection
if (reconnect && reconnectAttemptsRef.current < maxReconnectAttempts) {
reconnectAttemptsRef.current++;
const delay = reconnectDelay * Math.pow(2, reconnectAttemptsRef.current - 1);
setState(prev => ({
...prev,
status: 'disconnected',
error: new Error(`Reconnecting in ${delay/1000}s... (attempt ${reconnectAttemptsRef.current}/${maxReconnectAttempts})`),
}));
reconnectTimeoutRef.current = setTimeout(connect, delay);
}
};
}, [chain, address, onPrice, reconnect, reconnectDelay, maxReconnectAttempts]);
const disconnect = useCallback(() => {
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
}
if (eventSourceRef.current) {
eventSourceRef.current.close();
eventSourceRef.current = null;
}
setState({
price: null,
status: 'disconnected',
error: null,
lastUpdate: null,
});
reconnectAttemptsRef.current = 0;
}, []);
useEffect(() => {
connect();
return disconnect;
}, [connect, disconnect]);
return {
...state,
reconnect: connect,
disconnect,
};
}
3. Create Price Display Component
Createcomponents/CryptoPriceDisplay.tsx:
Show View complete component
Show View complete component
Copy
import React from 'react';
import { useCryptoStream } from '../hooks/useCryptoStream';
interface PriceDisplayProps {
chain: string;
address: string;
symbol: string;
decimals?: number;
className?: string;
}
export function CryptoPriceDisplay({
chain,
address,
symbol,
decimals = 2,
className = '',
}: PriceDisplayProps) {
const [previousPrice, setPreviousPrice] = React.useState<number | null>(null);
const { price, status, error, lastUpdate, reconnect } = useCryptoStream({
chain,
address,
onPrice: (newPrice) => {
setPreviousPrice(price);
},
});
const priceChange = previousPrice ? ((price ?? 0) - previousPrice) / previousPrice * 100 : 0;
const isUp = priceChange > 0;
const isDown = priceChange < 0;
return (
<div className={`crypto-price-display ${className}`}>
<div className="price-header">
<span className="symbol">{symbol}</span>
<span className={`status status-${status}`}>
{status === 'connected' && '🟢'}
{status === 'connecting' && '🟡'}
{status === 'error' && '🔴'}
{status === 'disconnected' && '⚪'}
</span>
</div>
<div className="price-content">
{price !== null ? (
<>
<div className={`price ${isUp ? 'up' : ''} ${isDown ? 'down' : ''}`}>
${price.toFixed(decimals)}
{isUp && ' ↑'}
{isDown && ' ↓'}
</div>
{priceChange !== 0 && (
<div className={`change ${isUp ? 'positive' : 'negative'}`}>
{priceChange > 0 ? '+' : ''}{priceChange.toFixed(2)}%
</div>
)}
</>
) : (
<div className="price loading">
{status === 'connecting' ? 'Connecting...' : '$---.--'}
</div>
)}
</div>
{error && (
<div className="error-message">
{error.message}
{status === 'error' && (
<button onClick={reconnect} className="retry-button">
Retry
</button>
)}
</div>
)}
{lastUpdate && (
<div className="last-update">
Last update: {lastUpdate.toLocaleTimeString()}
</div>
)}
<style jsx>{`
.crypto-price-display {
padding: 1rem;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
background: white;
}
.price-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.symbol {
font-weight: 600;
font-size: 1.125rem;
}
.price {
font-size: 2rem;
font-weight: bold;
transition: color 0.3s ease;
}
.price.up {
color: #16a34a;
}
.price.down {
color: #dc2626;
}
.change {
font-size: 0.875rem;
margin-top: 0.25rem;
}
.change.positive {
color: #16a34a;
}
.change.negative {
color: #dc2626;
}
.error-message {
margin-top: 0.5rem;
padding: 0.5rem;
background: #fee2e2;
color: #991b1b;
border-radius: 0.25rem;
font-size: 0.875rem;
}
.retry-button {
margin-left: 0.5rem;
padding: 0.25rem 0.5rem;
background: white;
border: 1px solid #991b1b;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.75rem;
}
.last-update {
margin-top: 0.5rem;
font-size: 0.75rem;
color: #6b7280;
}
`}</style>
</div>
);
}
4. Multi-Asset Streaming
For streaming multiple assets, createhooks/useMultiCryptoStream.ts:
Show View multi-asset streaming hook
Show View multi-asset streaming hook
Copy
import { useEffect, useRef, useState, useCallback } from 'react';
interface Asset {
chain: string;
address: string;
method: 't_p';
}
interface PriceUpdate {
chain: string;
address: string;
price: number;
timestamp: Date;
}
interface MultiStreamOptions {
assets: Asset[];
onUpdate?: (update: PriceUpdate) => void;
reconnect?: boolean;
}
export function useMultiCryptoStream({
assets,
onUpdate,
reconnect = true,
}: MultiStreamOptions) {
const [prices, setPrices] = useState<Map<string, PriceUpdate>>(new Map());
const [status, setStatus] = useState<'connecting' | 'connected' | 'error'>('connecting');
const abortControllerRef = useRef<AbortController>();
const connect = useCallback(async () => {
// Abort any existing connection
abortControllerRef.current?.abort();
abortControllerRef.current = new AbortController();
setStatus('connecting');
try {
const response = await fetch('https://streaming.dexpaprika.com/stream', {
method: 'POST',
headers: {
'Accept': 'text/event-stream',
'Content-Type': 'application/json',
},
body: JSON.stringify(assets),
signal: abortControllerRef.current.signal,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
setStatus('connected');
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const events = buffer.split('\n\n');
buffer = events.pop() || '';
for (const event of events) {
const lines = event.split('\n');
let data: string | null = null;
for (const line of lines) {
if (line.startsWith('data: ')) {
data = line.slice(6);
}
}
if (data) {
try {
const parsed = JSON.parse(data);
const update: PriceUpdate = {
chain: parsed.c,
address: parsed.a,
price: parseFloat(parsed.p),
timestamp: new Date(parsed.t * 1000),
};
setPrices(prev => {
const next = new Map(prev);
next.set(`${update.chain}:${update.address}`, update);
return next;
});
onUpdate?.(update);
} catch (error) {
console.error('Parse error:', error);
}
}
}
}
} catch (error: any) {
if (error.name !== 'AbortError') {
console.error('Stream error:', error);
setStatus('error');
if (reconnect) {
setTimeout(connect, 5000);
}
}
}
}, [assets, onUpdate, reconnect]);
useEffect(() => {
connect();
return () => {
abortControllerRef.current?.abort();
};
}, [connect]);
return { prices: Array.from(prices.values()), status };
}
Next.js Specific Considerations
App Router (Next.js 13+)
For Next.js App Router, ensure streaming components are client-side:Copy
// app/components/LivePrices.tsx
'use client';
import { CryptoPriceDisplay } from './CryptoPriceDisplay';
export default function LivePrices() {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<CryptoPriceDisplay
chain="ethereum"
address="0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
symbol="WETH"
/>
<CryptoPriceDisplay
chain="solana"
address="So11111111111111111111111111111111111111112"
symbol="SOL"
/>
<CryptoPriceDisplay
chain="bsc"
address="0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c"
symbol="BNB"
/>
</div>
);
}
Pages Router
For traditional Pages Router, handle SSR properly:Copy
// pages/prices.tsx
import dynamic from 'next/dynamic';
const LivePrices = dynamic(() => import('../components/LivePrices'), {
ssr: false, // Disable SSR for streaming components
loading: () => <p>Loading prices...</p>,
});
export default function PricesPage() {
return (
<div>
<h1>Live Crypto Prices</h1>
<LivePrices />
</div>
);
}
Advanced Patterns
1. Context Provider for Global Streaming
Copy
// contexts/CryptoStreamContext.tsx
import React, { createContext, useContext } from 'react';
import { useMultiCryptoStream } from '../hooks/useMultiCryptoStream';
const CryptoStreamContext = createContext<ReturnType<typeof useMultiCryptoStream> | null>(null);
export function CryptoStreamProvider({ children }: { children: React.ReactNode }) {
const streamData = useMultiCryptoStream({
assets: [
{ chain: 'ethereum', address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', method: 't_p' },
{ chain: 'solana', address: 'So11111111111111111111111111111111111111112', method: 't_p' },
// Add more assets as needed
],
});
return (
<CryptoStreamContext.Provider value={streamData}>
{children}
</CryptoStreamContext.Provider>
);
}
export function useCryptoStreamContext() {
const context = useContext(CryptoStreamContext);
if (!context) {
throw new Error('useCryptoStreamContext must be used within CryptoStreamProvider');
}
return context;
}
2. Optimistic UI Updates
Copy
function OptimisticPriceDisplay({ chain, address }: Props) {
const [displayPrice, setDisplayPrice] = useState<number | null>(null);
const [isStale, setIsStale] = useState(false);
const { price, lastUpdate } = useCryptoStream({
chain,
address,
onPrice: (newPrice) => {
setDisplayPrice(newPrice);
setIsStale(false);
},
});
useEffect(() => {
// Mark as stale if no update for 5 seconds
const timer = setTimeout(() => setIsStale(true), 5000);
return () => clearTimeout(timer);
}, [lastUpdate]);
return (
<div className={isStale ? 'opacity-50' : ''}>
${displayPrice?.toFixed(2) ?? '---'}
{isStale && <span className="text-yellow-500 text-xs">⚠️ Stale</span>}
</div>
);
}
3. Performance Optimization with React.memo
Copy
const PriceCell = React.memo(({ price, symbol }: { price: number; symbol: string }) => {
return (
<div className="price-cell">
<span className="symbol">{symbol}</span>
<span className="price">${price.toFixed(2)}</span>
</div>
);
}, (prevProps, nextProps) => {
// Only re-render if price changes by more than 0.01%
return Math.abs(prevProps.price - nextProps.price) / prevProps.price < 0.0001;
});
Testing
Unit Testing with Jest
Show View test implementation
Show View test implementation
Copy
// __tests__/useCryptoStream.test.ts
import { renderHook, act } from '@testing-library/react-hooks';
import { useCryptoStream } from '../hooks/useCryptoStream';
// Mock EventSource
global.EventSource = jest.fn(() => ({
addEventListener: jest.fn(),
close: jest.fn(),
onopen: jest.fn(),
onerror: jest.fn(),
})) as any;
describe('useCryptoStream', () => {
it('should connect on mount', () => {
const { result } = renderHook(() =>
useCryptoStream({
chain: 'ethereum',
address: '0xtest',
})
);
expect(result.current.status).toBe('connecting');
});
it('should update price on event', () => {
const { result } = renderHook(() =>
useCryptoStream({
chain: 'ethereum',
address: '0xtest',
})
);
act(() => {
// Simulate price update
const event = new MessageEvent('t_p', {
data: JSON.stringify({
a: '0xtest',
c: 'ethereum',
p: '1234.56',
t: Date.now() / 1000,
}),
});
// Trigger event handler
const eventSource = (global.EventSource as jest.Mock).mock.results[0].value;
eventSource.addEventListener.mock.calls[0][1](event);
});
expect(result.current.price).toBe(1234.56);
});
});
Production Checklist
Error Boundaries
Error Boundaries
Wrap streaming components in error boundaries to handle failures gracefully:
Copy
import { ErrorBoundary } from 'react-error-boundary';
<ErrorBoundary fallback={<div>Price unavailable</div>}>
<CryptoPriceDisplay {...props} />
</ErrorBoundary>
Memory Leaks
Memory Leaks
Always clean up EventSource connections and timeouts:
Copy
useEffect(() => {
const eventSource = new EventSource(url);
// Cleanup function
return () => {
eventSource.close();
clearTimeout(reconnectTimeout);
};
}, []);
Network Monitoring
Network Monitoring
Monitor connection status and alert on failures:
Copy
useEffect(() => {
if (status === 'error') {
// Send to error tracking service
Sentry.captureException(new Error('Stream connection failed'));
}
}, [status]);
Bundle Size
Bundle Size
Consider lazy loading streaming components:
Copy
const LivePrices = lazy(() => import('./LivePrices'));