> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dexpaprika.com/llms.txt
> Use this file to discover all available pages before exploring further.

# React & Next.js Integration

> Build real-time price components for React and Next.js applications with TypeScript support

> **[Production example: live streaming dashboard](https://dexpaprika.com/streaming/solana)**
>
> **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 /sse/prices 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 →](https://dexpaprika.com/streaming/solana)

## 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](https://docs.dexpaprika.com/api-reference/search/search-for-tokens-pools-and-dexes.md) to find tokens by name or symbol

> **Get Networks**
>
> Use the [Networks API](https://docs.dexpaprika.com/api-reference/networks/get-a-list-of-available-blockchain-networks.md) to get supported chains

```bash theme={null}
# Search for USDC tokens
curl "https://api.dexpaprika.com/search?query=USDC"

# Get all supported networks
curl "https://api.dexpaprika.com/networks"
```

***

## Quick example

```tsx React Hook theme={null}
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/sse/prices');
    url.searchParams.set('method', 'token_price');
    url.searchParams.set('chain', chain);
    url.searchParams.set('address', address);

    const eventSource = new EventSource(url.toString());

    eventSource.onopen = () => setStatus('connected');

    eventSource.addEventListener('token_price', (event) => {
      const data = JSON.parse(event.data);
      setPrice(parseFloat(data.price));
    });

    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>
  );
}
```

```tsx Next.js Component theme={null}
'use client'; // For Next.js 13+ App Router

import { useEffect, useState } from 'react';

export default function LivePrice({ chain, address }: { chain: string; address: string }) {
  const [price, setPrice] = useState<number | null>(null);
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    setIsClient(true); // Ensure client-side only
  }, []);

  useEffect(() => {
    if (!isClient) return;

    const url = new URL('https://streaming.dexpaprika.com/sse/prices');
    url.searchParams.set('method', 'token_price');
    url.searchParams.set('chain', chain);
    url.searchParams.set('address', address);

    const eventSource = new EventSource(url.toString());

    eventSource.addEventListener('token_price', (event) => {
      const data = JSON.parse(event.data);
      setPrice(parseFloat(data.price));
    });

    return () => eventSource.close();
  }, [isClient, chain, address]);

  if (!isClient) return <span>Loading...</span>;

  return <span>${price?.toFixed(2) ?? '--'}</span>;
}
```

***

## Complete implementation

### 1. Install dependencies

```bash theme={null}
npm install --save-dev @types/react
```

### 2. Create the streaming hook

Create `hooks/useCryptoStream.ts`:

**View complete hook implementation**

```typescript theme={null}
import { useEffect, useRef, useState, useCallback } from 'react';

interface PriceData {
  address: string;
  chain: string;
  price: string;            // USD price, kept as string for precision
  timestamp: number;        // unix seconds, server send time
  timestamp_price: number;  // unix seconds, observation time
  token_price: number;      // alias of timestamp_price
}

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/sse/prices');
    url.searchParams.set('method', 'token_price');
    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('token_price', (event: MessageEvent) => {
      try {
        const data: PriceData = JSON.parse(event.data);
        const price = parseFloat(data.price);

        setState(prev => ({
          ...prev,
          price,
          lastUpdate: new Date(data.timestamp * 1000),
        }));

        onPrice?.(price);
      } catch (error) {
        console.error('Failed to parse price data:', error);
      }
    });

    // Server may push deprecation notices and similar non-fatal signals.
    eventSource.addEventListener('warning', (event: MessageEvent) => {
      try {
        const { message } = JSON.parse(event.data);
        console.warn('[stream warning]', message);
      } catch {
        // ignore malformed warning payloads
      }
    });

    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

Create `components/CryptoPriceDisplay.tsx`:

**View complete component**

```tsx theme={null}
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}
        </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, create `hooks/useMultiCryptoStream.ts`:

**View multi-asset streaming hook**

```typescript theme={null}
import { useEffect, useRef, useState, useCallback } from 'react';

interface Asset {
  chain: string;
  address: string;
  method: 'token_price';
}

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/sse/prices', {
        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 decoder = new TextDecoder();
      let buffer = '';

      // for-await over the response body handles gzip/chunked encodings
      // transparently in Node and modern browsers.
      for await (const chunk of response.body as any) {
        buffer += decoder.decode(chunk, { stream: true });

        const messages = buffer.split('\n\n');
        buffer = messages.pop() ?? '';

        for (const message of messages) {
          const lines = message.split('\n');
          const eventLine = lines.find(l => l.startsWith('event:'));
          const dataLine  = lines.find(l => l.startsWith('data:'));
          if (!dataLine) continue;

          const eventType = eventLine ? eventLine.slice(6).trim() : 'message';
          if (eventType !== 'token_price') continue; // skip ping/warning/etc

          try {
            const parsed = JSON.parse(dataLine.slice(5).trim());
            const update: PriceUpdate = {
              chain: parsed.chain,
              address: parsed.address,
              price: parseFloat(parsed.price),
              timestamp: new Date(parsed.timestamp * 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:

```tsx theme={null}
// 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:

```tsx theme={null}
// 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

```tsx theme={null}
// 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: 'token_price' },
      { chain: 'solana',   address: 'So11111111111111111111111111111111111111112',  method: 'token_price' },
      // 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

```tsx theme={null}
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

```tsx theme={null}
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

**View test implementation**

```typescript theme={null}
// __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('token_price', {
        data: JSON.stringify({
          address: '0xtest',
          chain: 'ethereum',
          price: '1234.56',
          timestamp: Math.floor(Date.now() / 1000),
          timestamp_price: Math.floor(Date.now() / 1000),
          token_price: Math.floor(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**
>
> Wrap streaming components in error boundaries to handle failures gracefully:
>
> ```tsx theme={null}
> import { ErrorBoundary } from 'react-error-boundary';
>
> <ErrorBoundary fallback={<div>Price unavailable</div>}>
>   <CryptoPriceDisplay {...props} />
> </ErrorBoundary>
> ```

> **Memory Leaks**
>
> Always clean up EventSource connections and timeouts:
>
> ```tsx theme={null}
> useEffect(() => {
>   const eventSource = new EventSource(url);
>
>   // Cleanup function
>   return () => {
>     eventSource.close();
>     clearTimeout(reconnectTimeout);
>   };
> }, []);
> ```

> **Network Monitoring**
>
> Monitor connection status and alert on failures:
>
> ```tsx theme={null}
> useEffect(() => {
>   if (status === 'error') {
>     // Send to error tracking service
>     Sentry.captureException(new Error('Stream connection failed'));
>   }
> }, [status]);
> ```

> **Bundle Size**
>
> Consider lazy loading streaming components:
>
> ```tsx theme={null}
> const LivePrices = lazy(() => import('./LivePrices'));
> ```

***

## Get support

> **[Discord Community](https://discord.gg/DhJge5TUGM)**
>
> Get help from other React developers.

> **[Email Support](mailto:support@coinpaprika.com)**
>
> Report bugs or request features.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.