⚠️ Alpha内测版本警告:此为早期内部构建版本,尚不完整且可能存在错误,欢迎大家提Issue反馈问题或建议
Skip to content

3.7.4 错误重试

一句话破题

失败不可怕,可怕的是用户不知道怎么重试。给用户一个按钮,胜过一句"请稍后再试"。

核心价值

网络请求失败是常态。好的错误处理应该:告诉用户发生了什么、提供立即重试的方式、必要时自动重试。

错误状态组件

tsx
// components/ErrorState.tsx
import { ReactNode } from 'react'

interface ErrorStateProps {
  title?: string
  message?: string
  onRetry?: () => void
  retrying?: boolean
  action?: ReactNode
}

export function ErrorState({
  title = '加载失败',
  message = '请检查网络连接后重试',
  onRetry,
  retrying = false,
  action,
}: ErrorStateProps) {
  return (
    <div className="flex flex-col items-center justify-center py-12">
      <div className="w-16 h-16 mb-4 text-red-500">
        <AlertCircleIcon className="w-full h-full" />
      </div>
      
      <h3 className="text-lg font-medium text-gray-900 mb-2">
        {title}
      </h3>
      
      <p className="text-sm text-gray-500 mb-6 text-center max-w-sm">
        {message}
      </p>
      
      {action || (onRetry && (
        <button
          onClick={onRetry}
          disabled={retrying}
          className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
        >
          {retrying ? '重试中...' : '重试'}
        </button>
      ))}
    </div>
  )
}

在列表中使用

tsx
function PostList() {
  const { data, error, isLoading, refetch, isRefetching } = usePosts()
  
  if (isLoading) {
    return <PostListSkeleton />
  }
  
  if (error) {
    return (
      <ErrorState
        title="加载文章失败"
        message={error.message}
        onRetry={refetch}
        retrying={isRefetching}
      />
    )
  }
  
  if (data.length === 0) {
    return <Empty title="暂无文章" />
  }
  
  return (
    <ul className="space-y-4">
      {data.map((post) => (
        <PostCard key={post.id} post={post} />
      ))}
    </ul>
  )
}

自动重试

tsx
// hooks/useAutoRetry.ts
import { useState, useCallback } from 'react'

interface UseAutoRetryOptions {
  maxRetries?: number
  retryDelay?: number
  onMaxRetriesReached?: () => void
}

export function useAutoRetry<T>(
  fetcher: () => Promise<T>,
  options: UseAutoRetryOptions = {}
) {
  const { maxRetries = 3, retryDelay = 1000, onMaxRetriesReached } = options
  
  const [data, setData] = useState<T | null>(null)
  const [error, setError] = useState<Error | null>(null)
  const [isLoading, setIsLoading] = useState(false)
  const [retryCount, setRetryCount] = useState(0)
  
  const execute = useCallback(async () => {
    setIsLoading(true)
    setError(null)
    
    let lastError: Error | null = null
    
    for (let i = 0; i <= maxRetries; i++) {
      try {
        const result = await fetcher()
        setData(result)
        setRetryCount(0)
        setIsLoading(false)
        return result
      } catch (e) {
        lastError = e as Error
        setRetryCount(i)
        
        if (i < maxRetries) {
          await new Promise((r) => setTimeout(r, retryDelay * (i + 1)))
        }
      }
    }
    
    setError(lastError)
    setIsLoading(false)
    onMaxRetriesReached?.()
    throw lastError
  }, [fetcher, maxRetries, retryDelay, onMaxRetriesReached])
  
  return { data, error, isLoading, retryCount, execute }
}

带重试的 fetch

tsx
// lib/fetchWithRetry.ts
interface RetryOptions {
  retries?: number
  retryDelay?: number
  retryOn?: number[]
}

export async function fetchWithRetry(
  url: string,
  options?: RequestInit,
  retryOptions: RetryOptions = {}
) {
  const { 
    retries = 3, 
    retryDelay = 1000,
    retryOn = [500, 502, 503, 504]
  } = retryOptions
  
  let lastError: Error
  
  for (let i = 0; i <= retries; i++) {
    try {
      const response = await fetch(url, options)
      
      if (!response.ok && retryOn.includes(response.status)) {
        throw new Error(`HTTP ${response.status}`)
      }
      
      return response
    } catch (error) {
      lastError = error as Error
      
      if (i < retries) {
        await new Promise((r) => setTimeout(r, retryDelay * Math.pow(2, i)))
      }
    }
  }
  
  throw lastError!
}

// 使用
const response = await fetchWithRetry('/api/posts', {}, {
  retries: 3,
  retryDelay: 1000,
  retryOn: [500, 502, 503, 504],
})

Toast 提示重试

tsx
// hooks/useMutationWithRetry.ts
import { toast } from 'sonner'

export function useMutationWithRetry<TData, TVariables>(
  mutationFn: (variables: TVariables) => Promise<TData>
) {
  const [isLoading, setIsLoading] = useState(false)
  
  const mutate = async (variables: TVariables) => {
    setIsLoading(true)
    
    try {
      const result = await mutationFn(variables)
      toast.success('操作成功')
      return result
    } catch (error) {
      toast.error('操作失败', {
        action: {
          label: '重试',
          onClick: () => mutate(variables),
        },
      })
      throw error
    } finally {
      setIsLoading(false)
    }
  }
  
  return { mutate, isLoading }
}

// 使用
function SubmitButton() {
  const { mutate, isLoading } = useMutationWithRetry(submitForm)
  
  return (
    <Button onClick={() => mutate(formData)} loading={isLoading}>
      提交
    </Button>
  )
}

离线状态处理

tsx
// hooks/useOnlineStatus.ts
import { useState, useEffect } from 'react'

export function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(
    typeof navigator !== 'undefined' ? navigator.onLine : true
  )
  
  useEffect(() => {
    const handleOnline = () => setIsOnline(true)
    const handleOffline = () => setIsOnline(false)
    
    window.addEventListener('online', handleOnline)
    window.addEventListener('offline', handleOffline)
    
    return () => {
      window.removeEventListener('online', handleOnline)
      window.removeEventListener('offline', handleOffline)
    }
  }, [])
  
  return isOnline
}

// 离线提示组件
export function OfflineBanner() {
  const isOnline = useOnlineStatus()
  
  if (isOnline) return null
  
  return (
    <div className="fixed bottom-4 left-4 right-4 bg-yellow-500 text-white p-4 rounded-lg shadow-lg">
      <p className="font-medium">网络已断开</p>
      <p className="text-sm">请检查网络连接,恢复后将自动重试</p>
    </div>
  )
}

在线恢复后自动重试

tsx
// hooks/useRetryOnReconnect.ts
import { useEffect, useRef } from 'react'
import { useOnlineStatus } from './useOnlineStatus'

export function useRetryOnReconnect(callback: () => void) {
  const isOnline = useOnlineStatus()
  const wasOffline = useRef(false)
  
  useEffect(() => {
    if (!isOnline) {
      wasOffline.current = true
    } else if (wasOffline.current) {
      wasOffline.current = false
      callback()
    }
  }, [isOnline, callback])
}

// 使用
function PostList() {
  const { refetch } = usePosts()
  
  useRetryOnReconnect(refetch)
  
  // ...
}

错误恢复策略

AI 协作指南

核心意图:让 AI 帮你实现健壮的错误重试机制。

需求定义公式

  • 功能描述:为 [场景] 实现错误重试
  • 重试策略:[自动/手动/混合]
  • 用户反馈:[Toast/内联提示/页面状态]

示例 Prompt

请实现一个带重试功能的数据获取 Hook:
1. 自动重试 3 次,每次间隔翻倍(指数退避)
2. 只对 5xx 错误和网络错误重试
3. 显示当前重试次数
4. 网络恢复后自动重试

验收清单

  • [ ] 失败请求有明确的错误提示
  • [ ] 提供手动重试按钮
  • [ ] 关键请求有自动重试
  • [ ] 离线状态有提示