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
}
| 参数 | 类型 | 默认值 | 说明 |
|---|
| symbol | string | 'BTC/USDT' | 交易对(当前实现中服务端推送固定为 BTC/USDT,可扩展) |
| enabled | boolean | true | 是否建立 SSE 连接 |
| 返回值 | 类型 | 说明 |
|---|
| status | 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 连接状态 |
| lastQuote | { type, symbol, price, ts } | null | 最新一条行情 |
| error | string | 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 仅做增量更新,保证首屏快、实时性不丢。