> ## 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

<Card title="🎯 Production Example: Live Streaming Dashboard" icon="react" href="https://crypto-streaming-dashboard-v3.vercel.app/" color="#10B981">
  **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 →](https://crypto-streaming-dashboard-v3.vercel.app/)
</Card>

## 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:

<CardGroup cols={2}>
  <Card title="Search Tokens" icon="search">
    Use the [Search API](/api-reference/search/search-for-tokens-pools-and-dexes) to find tokens by name or symbol
  </Card>

  <Card title="Get Networks" icon="globe">
    Use the [Networks API](/api-reference/networks/get-a-list-of-available-blockchain-networks) to get supported chains
  </Card>
</CardGroup>

```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

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

  ```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/stream');
      url.searchParams.set('method', 't_p');
      url.searchParams.set('chain', chain);
      url.searchParams.set('address', address);

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

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

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

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

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

***

## Complete Implementation

### 1. Install Dependencies

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

### 2. Create the Streaming Hook

Create `hooks/useCryptoStream.ts`:

<Expandable title="View complete hook implementation">
  ```typescript theme={null}
  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,
    };
  }
  ```
</Expandable>

### 3. Create Price Display Component

Create `components/CryptoPriceDisplay.tsx`:

<Expandable title="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 === '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>
    );
  }
  ```
</Expandable>

### 4. Multi-Asset Streaming

For streaming multiple assets, create `hooks/useMultiCryptoStream.ts`:

<Expandable title="View multi-asset streaming hook">
  ```typescript theme={null}
  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 };
  }
  ```
</Expandable>

***

## 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: '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

```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

<Expandable title="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('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);
    });
  });
  ```
</Expandable>

***

## Production Checklist

<AccordionGroup>
  <Accordion title="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>
    ```
  </Accordion>

  <Accordion title="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);
      };
    }, []);
    ```
  </Accordion>

  <Accordion title="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]);
    ```
  </Accordion>

  <Accordion title="Bundle Size">
    Consider lazy loading streaming components:

    ```tsx theme={null}
    const LivePrices = lazy(() => import('./LivePrices'));
    ```
  </Accordion>
</AccordionGroup>

***

## Get Support

<CardGroup cols={2}>
  <Card title="Discord Community" icon="discord" href="https://discord.gg/DhJge5TUGM">
    Get help from other React developers.
  </Card>

  <Card title="Email Support" icon="envelope" href="mailto:support@coinpaprika.com">
    Report bugs or request features.
  </Card>
</CardGroup>
