useSSEQuotes Hook 说明

SSE 实时行情 Hook,用于在交易页订阅实时价格推送,解耦首屏与实时流:首屏用 React Query 静态数据,实时行情通过本 Hook 单独订阅。

特性(P7+ 亮点)

  • 断线指数退避重连:重连间隔从 1s 起,按 2 倍递增,上限 30s,避免雪崩
  • 连接状态idle | connecting | connected | reconnecting | disconnected,便于 UI 展示连接状态
  • 按需启用enabled 为 false 或未在浏览器环境时不建立连接
  • 卸载清理:组件卸载时关闭 EventSource、清除定时器,无泄漏

API

function useSSEQuotes(symbol?: string, enabled?: boolean): {
  status: SSEConnectionStatus
  lastQuote: QuoteMessage | null
  error: string | null
}
参数类型默认值说明
symbolstring'BTC/USDT'交易对(当前实现中服务端推送固定为 BTC/USDT,可扩展)
enabledbooleantrue是否建立 SSE 连接
返回值类型说明
status'idle' | 'connecting' | 'connected' | 'reconnecting' | 'disconnected'连接状态
lastQuote{ type, symbol, price, ts } | null最新一条行情
errorstring | null错误信息(如重连提示)

使用示例

import { useSSEQuotes } from '@/hooks/useSSEQuotes'

function TradingPanel() {
  const { status, lastQuote, error } = useSSEQuotes('BTC/USDT', true)

  return (
    <div>
      <span className={status === 'connected' ? 'text-green-500' : 'text-amber-500'}>
        {status === 'connected' && lastQuote
          ? `Live: ${lastQuote.symbol} ${lastQuote.price}`
          : status === 'reconnecting'
            ? 'Reconnecting…'
            : 'SSE /api/sse/quotes'}
      </span>
      {error && <p className="text-red-400">{error}</p>}
    </div>
  )
}

源码

'use client'

import { useEffect, useRef, useState } from 'react'

export type SSEConnectionStatus = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'disconnected'

export interface QuoteMessage {
  type: string
  symbol: string
  price: string
  ts: number
}

const MIN_BACKOFF = 1000
const MAX_BACKOFF = 30_000
const BACKOFF_MULTIPLIER = 2

/**
 * SSE 实时行情 Hook(P7+ 亮点:断线指数退避重连 + 连接状态)
 * 解耦首屏与实时流,重连期间可继续用 React Query 缓存展示
 */
export function useSSEQuotes(symbol: string = 'BTC/USDT', enabled = true) {
  const [status, setStatus] = useState<SSEConnectionStatus>('idle')
  const [lastQuote, setLastQuote] = useState<QuoteMessage | null>(null)
  const [error, setError] = useState<string | null>(null)
  const backoffRef = useRef(MIN_BACKOFF)
  const eventSourceRef = useRef<EventSource | null>(null)
  const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)

  useEffect(() => {
    if (!enabled || typeof window === 'undefined') return

    let mounted = true

    function connect() {
      if (!mounted) return
      const url = `${window.location.origin}/api/sse/quotes`
      setStatus('connecting')
      setError(null)
      const es = new EventSource(url)
      eventSourceRef.current = es

      es.onopen = () => {
        if (!mounted) return
        setStatus('connected')
        backoffRef.current = MIN_BACKOFF
      }

      es.onmessage = (e) => {
        if (!mounted) return
        try {
          const data = JSON.parse(e.data) as QuoteMessage
          setLastQuote(data)
        } catch {
          // ignore parse error
        }
      }

      es.onerror = () => {
        if (!mounted) return
        es.close()
        eventSourceRef.current = null
        setStatus('reconnecting')
        setError('Connection lost, reconnecting…')
        const delay = Math.min(backoffRef.current, MAX_BACKOFF)
        timeoutRef.current = setTimeout(() => {
          backoffRef.current *= BACKOFF_MULTIPLIER
          connect()
        }, delay)
      }
    }

    connect()

    return () => {
      mounted = false
      if (timeoutRef.current) clearTimeout(timeoutRef.current)
      eventSourceRef.current?.close()
      eventSourceRef.current = null
      setStatus('disconnected')
    }
  }, [enabled, symbol])

  return { status, lastQuote, error }
}

面试要点

  • 为什么用 SSE 而不是 WebSocket:服务端单向推送行情即可,SSE 基于 HTTP、自动重连、易配合现有网关与 CDN。
  • 重连策略:指数退避避免断网恢复瞬间大量重连打满服务端。
  • 与首屏解耦:首屏不依赖 SSE,用 BFF/React Query 数据渲染;SSE 仅做增量更新,保证首屏快、实时性不丢。