Skip to content

乐观更新错误回滚

学习目标

通过本章学习,你将掌握:

  • 错误回滚机制
  • 自动回滚原理
  • 手动回滚处理
  • 错误提示策略
  • 重试机制
  • 部分回滚
  • 复杂场景处理
  • 用户体验优化

第一部分:自动回滚机制

1.1 useOptimistic的自动回滚

jsx
'use client';

import { useOptimistic, useState } from 'react';
import { likePost } from './actions';

export default function LikeButton({ postId, initialLikes }) {
  const [likes, setLikes] = useState(initialLikes);
  
  const [optimisticLikes, setOptimisticLikes] = useOptimistic(
    likes,
    (current, increment) => current + increment
  );
  
  const handleLike = async () => {
    // 步骤1:乐观更新(立即显示)
    setOptimisticLikes(1);  // likes + 1
    
    try {
      // 步骤2:发送请求
      const newLikes = await likePost(postId);
      
      // 步骤3:成功 - 更新实际状态
      setLikes(newLikes);
      
      // 此时optimisticLikes会自动同步到newLikes
    } catch (error) {
      // 步骤4:失败 - 自动回滚
      // optimisticLikes自动恢复到likes的值
      // 无需手动处理!
      
      console.error('点赞失败:', error);
    }
  };
  
  return (
    <button onClick={handleLike}>
      ❤️ {optimisticLikes}
    </button>
  );
}

// 工作原理:
// 成功:optimisticLikes跟随likes更新
// 失败:optimisticLikes自动恢复到likes

1.2 回滚时机

jsx
'use client';

import { useOptimistic, useState, useEffect } from 'react';

export default function AutoRollbackDemo() {
  const [value, setValue] = useState(0);
  const [logs, setLogs] = useState([]);
  
  const [optimisticValue, setOptimisticValue] = useOptimistic(
    value,
    (_, newValue) => newValue
  );
  
  const addLog = (message) => {
    setLogs(prev => [...prev, `${new Date().toLocaleTimeString()}: ${message}`]);
  };
  
  const handleUpdate = async (shouldFail) => {
    addLog(`乐观更新: ${value} → ${value + 1}`);
    setOptimisticValue(value + 1);
    
    // 模拟异步操作
    await new Promise(resolve => setTimeout(resolve, 1000));
    
    if (shouldFail) {
      addLog('操作失败,自动回滚');
      // 抛出错误触发回滚
      throw new Error('操作失败');
    } else {
      addLog(`操作成功,确认更新: ${value + 1}`);
      setValue(value + 1);
    }
  };
  
  return (
    <div>
      <p>实际值: {value}</p>
      <p>显示值: {optimisticValue}</p>
      
      <button onClick={() => handleUpdate(false).catch(() => {})}>
        成功更新
      </button>
      
      <button onClick={() => handleUpdate(true).catch(() => {})}>
        失败回滚
      </button>
      
      <div className="logs">
        {logs.map((log, i) => (
          <div key={i}>{log}</div>
        ))}
      </div>
    </div>
  );
}

1.3 多次乐观更新

jsx
'use client';

import { useOptimistic, useState } from 'react';

export default function MultipleOptimisticUpdates() {
  const [count, setCount] = useState(0);
  const [pending, setPending] = useState(0);
  
  const [optimisticCount, addOptimistic] = useOptimistic(
    count,
    (current, increment) => current + increment
  );
  
  const handleIncrement = async () => {
    setPending(prev => prev + 1);
    
    // 乐观更新
    addOptimistic(1);
    
    try {
      await new Promise((resolve, reject) => {
        setTimeout(() => {
          // 50%概率失败
          if (Math.random() > 0.5) {
            resolve();
          } else {
            reject(new Error('随机失败'));
          }
        }, 1000);
      });
      
      // 成功
      setCount(prev => prev + 1);
    } catch (error) {
      // 失败 - 自动回滚
      console.error('失败,已回滚');
    } finally {
      setPending(prev => prev - 1);
    }
  };
  
  return (
    <div>
      <p>计数: {optimisticCount}</p>
      <p>待确认: {pending}</p>
      
      <button onClick={handleIncrement}>
        增加(可能失败)
      </button>
    </div>
  );
}

// 说明:
// - 多次点击会创建多个乐观更新
// - 每个更新独立处理
// - 失败的会各自回滚
// - 成功的会保留

1.4 回滚动画效果

jsx
'use client';

import { useOptimistic, useState } from 'react';
import { motion } from 'framer-motion';

export default function AnimatedRollback({ postId, initialLikes }) {
  const [likes, setLikes] = useState(initialLikes);
  const [isRollingBack, setIsRollingBack] = useState(false);
  
  const [optimisticLikes, setOptimisticLikes] = useOptimistic(
    likes,
    (current, increment) => current + increment
  );
  
  const handleLike = async () => {
    setOptimisticLikes(1);
    
    try {
      const newLikes = await likePost(postId);
      setLikes(newLikes);
    } catch (error) {
      // 触发回滚动画
      setIsRollingBack(true);
      
      // 动画结束后清除状态
      setTimeout(() => setIsRollingBack(false), 500);
    }
  };
  
  return (
    <motion.button
      onClick={handleLike}
      animate={isRollingBack ? {
        x: [0, -10, 10, -10, 10, 0],
        rotate: [0, -5, 5, -5, 5, 0]
      } : {}}
      transition={{ duration: 0.5 }}
      className={isRollingBack ? 'rolling-back' : ''}
    >
      ❤️ {optimisticLikes}
    </motion.button>
  );
}

// CSS
/*
.rolling-back {
  background-color: #ffebee;
  color: #c62828;
}
*/

1.5 回滚状态追踪

jsx
'use client';

import { useOptimistic, useState, useRef } from 'react';

export default function RollbackTracking({ initialData }) {
  const [data, setData] = useState(initialData);
  const [rollbackHistory, setRollbackHistory] = useState([]);
  const attemptId = useRef(0);
  
  const [optimisticData, setOptimisticData] = useOptimistic(
    data,
    (current, update) => ({ ...current, ...update })
  );
  
  const handleUpdate = async (updates) => {
    const currentAttemptId = ++attemptId.current;
    const timestamp = new Date();
    
    // 乐观更新
    setOptimisticData(updates);
    
    try {
      const result = await updateData(updates);
      setData(result);
      
      // 记录成功
      setRollbackHistory(prev => [...prev, {
        id: currentAttemptId,
        timestamp,
        status: 'success',
        updates
      }]);
    } catch (error) {
      // 记录回滚
      setRollbackHistory(prev => [...prev, {
        id: currentAttemptId,
        timestamp,
        status: 'rollback',
        updates,
        error: error.message
      }]);
    }
  };
  
  return (
    <div>
      <div className="data-display">
        <p>当前值: {optimisticData.value}</p>
        <button onClick={() => handleUpdate({ value: optimisticData.value + 1 })}>
          增加
        </button>
      </div>
      
      <div className="history">
        <h3>操作历史</h3>
        {rollbackHistory.slice().reverse().map(record => (
          <div 
            key={record.id}
            className={`history-item ${record.status}`}
          >
            <span className="time">
              {record.timestamp.toLocaleTimeString()}
            </span>
            <span className="status">
              {record.status === 'success' ? '✓ 成功' : '✗ 回滚'}
            </span>
            {record.error && (
              <span className="error">{record.error}</span>
            )}
          </div>
        ))}
      </div>
    </div>
  );
}

第二部分:错误提示

2.1 显示错误消息

jsx
'use client';

import { useOptimistic, useState } from 'react';
import { updateTodo } from './actions';

export default function TodoWithErrorMessage({ todo }) {
  const [completed, setCompleted] = useState(todo.completed);
  const [error, setError] = useState(null);
  
  const [optimisticCompleted, setOptimisticCompleted] = useOptimistic(
    completed,
    (_, newValue) => newValue
  );
  
  const handleToggle = async () => {
    setError(null);
    
    // 乐观更新
    setOptimisticCompleted(!completed);
    
    try {
      await updateTodo(todo.id, !completed);
      setCompleted(!completed);
    } catch (error) {
      // 回滚后显示错误
      setError(error.message);
      
      // 3秒后清除错误消息
      setTimeout(() => setError(null), 3000);
    }
  };
  
  return (
    <div>
      <div className="todo-item">
        <input
          type="checkbox"
          checked={optimisticCompleted}
          onChange={handleToggle}
        />
        <span>{todo.text}</span>
      </div>
      
      {error && (
        <div className="error-toast">
          {error}
        </div>
      )}
    </div>
  );
}

2.2 内联错误提示

jsx
'use client';

import { useOptimistic, useState } from 'react';

export default function InlineError({ postId, initialLikes }) {
  const [likes, setLikes] = useState(initialLikes);
  const [error, setError] = useState(null);
  
  const [optimisticLikes, setOptimisticLikes] = useOptimistic(
    likes,
    (current, increment) => current + increment
  );
  
  const handleLike = async () => {
    setError(null);
    setOptimisticLikes(1);
    
    try {
      const newLikes = await likePost(postId);
      setLikes(newLikes);
    } catch (error) {
      setError('点赞失败,请重试');
    }
  };
  
  return (
    <div className="like-button-wrapper">
      <button onClick={handleLike} className={error ? 'error' : ''}>
        ❤️ {optimisticLikes}
      </button>
      
      {error && (
        <span className="error-message">
          {error}
        </span>
      )}
    </div>
  );
}

/* CSS */
.like-button-wrapper {
  position: relative;
}

.like-button.error {
  animation: shake 0.5s;
}

.error-message {
  position: absolute;
  bottom: -25px;
  left: 0;
  color: red;
  font-size: 12px;
}

@keyframes shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-5px); }
  75% { transform: translateX(5px); }
}

2.3 Toast通知

jsx
'use client';

import { useOptimistic, useState } from 'react';
import { toast } from 'react-hot-toast';

export default function TodoWithToast({ todo }) {
  const [completed, setCompleted] = useState(todo.completed);
  
  const [optimisticCompleted, setOptimisticCompleted] = useOptimistic(
    completed,
    (_, newValue) => newValue
  );
  
  const handleToggle = async () => {
    // 乐观更新
    setOptimisticCompleted(!completed);
    
    // 显示加载Toast
    const toastId = toast.loading('更新中...');
    
    try {
      await updateTodo(todo.id, !completed);
      setCompleted(!completed);
      
      // 成功Toast
      toast.success('已更新!', { id: toastId });
    } catch (error) {
      // 失败Toast
      toast.error('更新失败,请重试', { id: toastId });
    }
  };
  
  return (
    <div>
      <input
        type="checkbox"
        checked={optimisticCompleted}
        onChange={handleToggle}
      />
      <span>{todo.text}</span>
    </div>
  );
}

2.4 错误分类提示

jsx
'use client';

import { useOptimistic, useState } from 'react';

export default function CategorizedErrors({ initialData }) {
  const [data, setData] = useState(initialData);
  const [error, setError] = useState(null);
  
  const [optimisticData, setOptimisticData] = useOptimistic(
    data,
    (current, updates) => ({ ...current, ...updates })
  );
  
  const getErrorMessage = (error) => {
    if (error.code === 'NETWORK_ERROR') {
      return {
        title: '网络错误',
        message: '网络连接失败,请检查您的网络设置',
        retry: true
      };
    }
    
    if (error.code === 'VALIDATION_ERROR') {
      return {
        title: '验证失败',
        message: error.message,
        retry: false
      };
    }
    
    if (error.code === 'PERMISSION_DENIED') {
      return {
        title: '权限不足',
        message: '您没有权限执行此操作',
        retry: false
      };
    }
    
    return {
      title: '操作失败',
      message: '发生未知错误,请稍后重试',
      retry: true
    };
  };
  
  const handleUpdate = async (updates) => {
    setError(null);
    setOptimisticData(updates);
    
    try {
      const result = await updateData(updates);
      setData(result);
    } catch (err) {
      const errorInfo = getErrorMessage(err);
      setError(errorInfo);
    }
  };
  
  return (
    <div>
      <div className="data-editor">
        <input 
          value={optimisticData.value} 
          onChange={(e) => handleUpdate({ value: e.target.value })}
        />
      </div>
      
      {error && (
        <div className="error-notification">
          <div className="error-header">
            <strong>{error.title}</strong>
          </div>
          <p>{error.message}</p>
          {error.retry && (
            <button onClick={() => handleUpdate(optimisticData)}>
              重试
            </button>
          )}
        </div>
      )}
    </div>
  );
}

2.5 错误统计与监控

jsx
'use client';

import { useOptimistic, useState, useEffect } from 'react';

export default function ErrorMonitoring({ initialData }) {
  const [data, setData] = useState(initialData);
  const [errorStats, setErrorStats] = useState({
    total: 0,
    network: 0,
    validation: 0,
    server: 0,
    lastError: null
  });
  
  const [optimisticData, setOptimisticData] = useOptimistic(
    data,
    (current, updates) => ({ ...current, ...updates })
  );
  
  const recordError = (error) => {
    setErrorStats(prev => ({
      ...prev,
      total: prev.total + 1,
      [error.type]: (prev[error.type] || 0) + 1,
      lastError: {
        type: error.type,
        message: error.message,
        timestamp: new Date()
      }
    }));
    
    // 发送错误到监控服务
    if (typeof window !== 'undefined' && window.analytics) {
      window.analytics.track('OptimisticUpdateFailed', {
        errorType: error.type,
        errorMessage: error.message
      });
    }
  };
  
  const handleUpdate = async (updates) => {
    setOptimisticData(updates);
    
    try {
      const result = await updateData(updates);
      setData(result);
    } catch (error) {
      recordError({
        type: error.type || 'unknown',
        message: error.message
      });
    }
  };
  
  return (
    <div>
      <div className="error-stats">
        <h3>错误统计</h3>
        <p>总错误数: {errorStats.total}</p>
        <p>网络错误: {errorStats.network}</p>
        <p>验证错误: {errorStats.validation}</p>
        <p>服务器错误: {errorStats.server}</p>
        
        {errorStats.lastError && (
          <div className="last-error">
            <h4>最近错误</h4>
            <p>类型: {errorStats.lastError.type}</p>
            <p>消息: {errorStats.lastError.message}</p>
            <p>时间: {errorStats.lastError.timestamp.toLocaleString()}</p>
          </div>
        )}
      </div>
      
      {/* 数据编辑UI */}
    </div>
  );
}

第三部分:重试机制

3.1 自动重试

jsx
'use client';

import { useOptimistic, useState } from 'react';

export default function AutoRetry({ postId, initialLikes }) {
  const [likes, setLikes] = useState(initialLikes);
  const [retryCount, setRetryCount] = useState(0);
  
  const [optimisticLikes, setOptimisticLikes] = useOptimistic(
    likes,
    (current, increment) => current + increment
  );
  
  const likeWithRetry = async (retries = 3) => {
    setOptimisticLikes(1);
    
    for (let i = 0; i <= retries; i++) {
      try {
        const newLikes = await likePost(postId);
        setLikes(newLikes);
        setRetryCount(0);
        return;  // 成功
      } catch (error) {
        if (i === retries) {
          // 最后一次重试也失败
          setRetryCount(0);
          alert('点赞失败,请稍后再试');
          // 回滚会自动发生
          return;
        }
        
        // 继续重试
        setRetryCount(i + 1);
        await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
      }
    }
  };
  
  return (
    <div>
      <button onClick={() => likeWithRetry()}>
        ❤️ {optimisticLikes}
      </button>
      
      {retryCount > 0 && (
        <span className="retrying">
          重试中 ({retryCount}/3)...
        </span>
      )}
    </div>
  );
}

3.2 手动重试

jsx
'use client';

import { useOptimistic, useState } from 'react';

export default function ManualRetry({ postId, initialLikes }) {
  const [likes, setLikes] = useState(initialLikes);
  const [error, setError] = useState(null);
  const [pending, setPending] = useState(false);
  
  const [optimisticLikes, setOptimisticLikes] = useOptimistic(
    likes,
    (current, increment) => current + increment
  );
  
  const attemptLike = async () => {
    setError(null);
    setPending(true);
    setOptimisticLikes(1);
    
    try {
      const newLikes = await likePost(postId);
      setLikes(newLikes);
    } catch (error) {
      setError('点赞失败');
      // 回滚会自动发生
    } finally {
      setPending(false);
    }
  };
  
  return (
    <div>
      <button onClick={attemptLike} disabled={pending}>
        ❤️ {optimisticLikes}
      </button>
      
      {error && (
        <div className="error-box">
          <span>{error}</span>
          <button onClick={attemptLike}>重试</button>
        </div>
      )}
    </div>
  );
}

3.3 指数退避重试

jsx
'use client';

import { useOptimistic, useState } from 'react';

async function retryWithBackoff(fn, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === maxRetries - 1) {
        throw error;
      }
      
      // 指数退避:1s, 2s, 4s
      const delay = Math.pow(2, i) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

export default function ExponentialBackoff({ postId, initialLikes }) {
  const [likes, setLikes] = useState(initialLikes);
  const [status, setStatus] = useState('idle');
  
  const [optimisticLikes, setOptimisticLikes] = useOptimistic(
    likes,
    (current, increment) => current + increment
  );
  
  const handleLike = async () => {
    setStatus('pending');
    setOptimisticLikes(1);
    
    try {
      const newLikes = await retryWithBackoff(() => likePost(postId));
      setLikes(newLikes);
      setStatus('success');
    } catch (error) {
      setStatus('error');
      setTimeout(() => setStatus('idle'), 3000);
    }
  };
  
  return (
    <div>
      <button onClick={handleLike} disabled={status === 'pending'}>
        ❤️ {optimisticLikes}
      </button>
      
      {status === 'pending' && <span>重试中...</span>}
      {status === 'error' && <span className="error">所有重试均失败</span>}
    </div>
  );
}

3.4 智能重试策略

jsx
'use client';

import { useOptimistic, useState, useRef } from 'react';

class RetryStrategy {
  constructor(maxRetries = 3, baseDelay = 1000) {
    this.maxRetries = maxRetries;
    this.baseDelay = baseDelay;
  }
  
  async execute(fn, onRetry) {
    for (let attempt = 0; attempt < this.maxRetries; attempt++) {
      try {
        return await fn();
      } catch (error) {
        if (attempt === this.maxRetries - 1) {
          throw error;
        }
        
        // 根据错误类型决定是否重试
        if (!this.shouldRetry(error)) {
          throw error;
        }
        
        const delay = this.getDelay(attempt, error);
        
        if (onRetry) {
          onRetry(attempt + 1, delay);
        }
        
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }
  
  shouldRetry(error) {
    // 网络错误和超时错误应该重试
    return error.code === 'NETWORK_ERROR' || 
           error.code === 'TIMEOUT' ||
           error.status >= 500;
  }
  
  getDelay(attempt, error) {
    // 服务器错误使用指数退避
    if (error.status >= 500) {
      return Math.pow(2, attempt) * this.baseDelay;
    }
    
    // 网络错误使用固定延迟
    return this.baseDelay;
  }
}

export default function SmartRetry({ initialData }) {
  const [data, setData] = useState(initialData);
  const [retryInfo, setRetryInfo] = useState(null);
  const retryStrategy = useRef(new RetryStrategy(3, 1000));
  
  const [optimisticData, setOptimisticData] = useOptimistic(
    data,
    (current, updates) => ({ ...current, ...updates })
  );
  
  const handleUpdate = async (updates) => {
    setOptimisticData(updates);
    setRetryInfo(null);
    
    try {
      const result = await retryStrategy.current.execute(
        () => updateData(updates),
        (attempt, delay) => {
          setRetryInfo({
            attempt,
            delay,
            message: `第 ${attempt} 次重试,${delay}ms 后执行`
          });
        }
      );
      
      setData(result);
      setRetryInfo(null);
    } catch (error) {
      toast.error('操作失败:' + error.message);
    }
  };
  
  return (
    <div>
      <input 
        value={optimisticData.value}
        onChange={(e) => handleUpdate({ value: e.target.value })}
      />
      
      {retryInfo && (
        <div className="retry-info">
          {retryInfo.message}
        </div>
      )}
    </div>
  );
}

3.5 重试队列管理

jsx
'use client';

import { useOptimistic, useState, useRef } from 'react';

class RetryQueue {
  constructor() {
    this.queue = [];
    this.processing = false;
  }
  
  async add(task) {
    this.queue.push(task);
    
    if (!this.processing) {
      await this.process();
    }
  }
  
  async process() {
    this.processing = true;
    
    while (this.queue.length > 0) {
      const task = this.queue.shift();
      
      try {
        await task.execute();
      } catch (error) {
        // 失败的任务重新加入队列
        if (task.retries < task.maxRetries) {
          task.retries++;
          this.queue.push(task);
          
          // 等待一段时间再重试
          await new Promise(resolve => 
            setTimeout(resolve, 1000 * task.retries)
          );
        } else {
          task.onError(error);
        }
      }
    }
    
    this.processing = false;
  }
}

export default function QueuedRetry({ initialTodos }) {
  const [todos, setTodos] = useState(initialTodos);
  const retryQueue = useRef(new RetryQueue());
  
  const [optimisticTodos, updateOptimistic] = useOptimistic(
    todos,
    (state, action) => {
      if (action.type === 'toggle') {
        return state.map(todo =>
          todo.id === action.id
            ? { ...todo, completed: !todo.completed }
            : todo
        );
      }
      return state;
    }
  );
  
  const handleToggle = (id) => {
    const todo = todos.find(t => t.id === id);
    
    // 立即乐观更新
    updateOptimistic({ type: 'toggle', id });
    
    // 加入重试队列
    retryQueue.current.add({
      execute: async () => {
        await updateTodo(id, !todo.completed);
        setTodos(prev => 
          prev.map(t => 
            t.id === id ? { ...t, completed: !t.completed } : t
          )
        );
      },
      retries: 0,
      maxRetries: 3,
      onError: (error) => {
        toast.error(`更新 "${todo.text}" 失败`);
      }
    });
  };
  
  return (
    <ul>
      {optimisticTodos.map(todo => (
        <li key={todo.id}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => handleToggle(todo.id)}
          />
          <span>{todo.text}</span>
        </li>
      ))}
    </ul>
  );
}

第四部分:部分回滚

4.1 列表部分回滚

jsx
'use client';

import { useOptimistic, useState } from 'react';

export default function PartialRollback({ initialTodos }) {
  const [todos, setTodos] = useState(initialTodos);
  
  const [optimisticTodos, updateOptimistic] = useOptimistic(
    todos,
    (state, { id, completed }) => 
      state.map(todo => 
        todo.id === id ? { ...todo, completed, pending: true } : todo
      )
  );
  
  const handleToggle = async (id) => {
    const todo = todos.find(t => t.id === id);
    
    // 乐观更新单个todo
    updateOptimistic({ id, completed: !todo.completed });
    
    try {
      await updateTodo(id, !todo.completed);
      
      // 成功:更新实际状态
      setTodos(prev => 
        prev.map(t => 
          t.id === id ? { ...t, completed: !t.completed } : t
        )
      );
    } catch (error) {
      // 失败:只回滚这一个todo
      // useOptimistic会自动处理
      alert(`更新 "${todo.text}" 失败`);
    }
  };
  
  return (
    <ul>
      {optimisticTodos.map(todo => (
        <li key={todo.id} className={todo.pending ? 'pending' : ''}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => handleToggle(todo.id)}
          />
          <span>{todo.text}</span>
        </li>
      ))}
    </ul>
  );
}

4.2 批量操作部分成功

jsx
'use client';

import { useOptimistic, useState } from 'react';

export default function BatchPartialSuccess({ initialTodos }) {
  const [todos, setTodos] = useState(initialTodos);
  const [selectedIds, setSelectedIds] = useState([]);
  const [failures, setFailures] = useState([]);
  
  const [optimisticTodos, updateOptimistic] = useOptimistic(
    todos,
    (state, completedIds) => 
      state.map(todo => 
        completedIds.includes(todo.id)
          ? { ...todo, completed: true, pending: true }
          : todo
      )
  );
  
  const handleBatchComplete = async () => {
    if (selectedIds.length === 0) return;
    
    // 乐观更新所有选中的
    updateOptimistic(selectedIds);
    
    try {
      // 批量处理(可能部分失败)
      const result = await batchUpdateTodos(selectedIds);
      
      // 更新成功的
      setTodos(prev => 
        prev.map(todo => 
          result.successful.includes(todo.id)
            ? { ...todo, completed: true }
            : todo
        )
      );
      
      // 记录失败的
      setFailures(result.failed);
      
      if (result.failed.length > 0) {
        // 失败的会自动回滚
        alert(`${result.failed.length} 项更新失败`);
      }
      
      // 清空选择
      setSelectedIds([]);
    } catch (error) {
      alert('批量操作失败');
      setFailures([]);
    }
  };
  
  return (
    <div>
      <button onClick={handleBatchComplete}>
        完成选中的 {selectedIds.length} 项
      </button>
      
      {failures.length > 0 && (
        <div className="error">
          以下项目更新失败:
          {failures.map(id => {
            const todo = todos.find(t => t.id === id);
            return <div key={id}>{todo?.text}</div>;
          })}
        </div>
      )}
      
      <ul>
        {optimisticTodos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={selectedIds.includes(todo.id)}
              onChange={(e) => {
                if (e.target.checked) {
                  setSelectedIds(prev => [...prev, todo.id]);
                } else {
                  setSelectedIds(prev => prev.filter(id => id !== todo.id));
                }
              }}
            />
            <span className={todo.completed ? 'completed' : ''}>
              {todo.text}
            </span>
            {todo.pending && <span>确认中...</span>}
          </li>
        ))}
      </ul>
    </div>
  );
}

4.3 嵌套数据部分回滚

jsx
'use client';

import { useOptimistic, useState } from 'react';

export default function NestedPartialRollback({ initialPost }) {
  const [post, setPost] = useState(initialPost);
  
  const [optimisticPost, updateOptimistic] = useOptimistic(
    post,
    (current, action) => {
      if (action.type === 'likeComment') {
        return {
          ...current,
          comments: current.comments.map(comment =>
            comment.id === action.commentId
              ? { 
                  ...comment, 
                  likes: comment.likes + 1,
                  pending: true
                }
              : comment
          )
        };
      }
      return current;
    }
  );
  
  const handleLikeComment = async (commentId) => {
    // 乐观更新评论点赞
    updateOptimistic({ type: 'likeComment', commentId });
    
    try {
      const result = await likeComment(post.id, commentId);
      
      // 成功:更新整个文章数据
      setPost(result);
    } catch (error) {
      // 失败:只回滚这个评论的点赞
      toast.error('点赞失败');
    }
  };
  
  return (
    <article>
      <h2>{optimisticPost.title}</h2>
      <p>{optimisticPost.content}</p>
      
      <section className="comments">
        {optimisticPost.comments.map(comment => (
          <div 
            key={comment.id}
            className={comment.pending ? 'pending' : ''}
          >
            <p>{comment.text}</p>
            <button onClick={() => handleLikeComment(comment.id)}>
              ❤️ {comment.likes}
            </button>
          </div>
        ))}
      </section>
    </article>
  );
}

4.4 分组回滚策略

jsx
'use client';

import { useOptimistic, useState } from 'react';

export default function GroupedRollback({ initialItems }) {
  const [items, setItems] = useState(initialItems);
  const [groupErrors, setGroupErrors] = useState({});
  
  const [optimisticItems, updateOptimistic] = useOptimistic(
    items,
    (state, action) => {
      if (action.type === 'updateGroup') {
        return state.map(item =>
          item.group === action.group
            ? { ...item, ...action.updates, pending: true }
            : item
        );
      }
      return state;
    }
  );
  
  const handleUpdateGroup = async (group, updates) => {
    // 乐观更新整组
    updateOptimistic({ type: 'updateGroup', group, updates });
    
    try {
      const result = await updateItemGroup(group, updates);
      
      // 成功:更新整组
      setItems(prev =>
        prev.map(item =>
          item.group === group
            ? { ...item, ...updates }
            : item
        )
      );
      
      // 清除该组的错误
      setGroupErrors(prev => {
        const newErrors = { ...prev };
        delete newErrors[group];
        return newErrors;
      });
    } catch (error) {
      // 失败:整组回滚
      setGroupErrors(prev => ({
        ...prev,
        [group]: error.message
      }));
    }
  };
  
  // 按组分类items
  const groupedItems = optimisticItems.reduce((acc, item) => {
    if (!acc[item.group]) {
      acc[item.group] = [];
    }
    acc[item.group].push(item);
    return acc;
  }, {});
  
  return (
    <div>
      {Object.entries(groupedItems).map(([group, items]) => (
        <div key={group} className="group">
          <h3>
            {group}
            {groupErrors[group] && (
              <span className="error">
                {groupErrors[group]}
              </span>
            )}
          </h3>
          
          <ul>
            {items.map(item => (
              <li 
                key={item.id}
                className={item.pending ? 'pending' : ''}
              >
                {item.name}
              </li>
            ))}
          </ul>
          
          <button 
            onClick={() => handleUpdateGroup(group, { status: 'completed' })}
          >
            完成整组
          </button>
        </div>
      ))}
    </div>
  );
}

第五部分:用户体验优化

5.1 视觉反馈

jsx
'use client';

import { useOptimistic, useState } from 'react';

export default function VisualFeedback({ initialData }) {
  const [data, setData] = useState(initialData);
  const [operationState, setOperationState] = useState('idle');
  
  const [optimisticData, setOptimisticData] = useOptimistic(
    data,
    (current, updates) => ({ ...current, ...updates })
  );
  
  const handleUpdate = async (updates) => {
    setOperationState('optimistic');
    setOptimisticData(updates);
    
    try {
      await new Promise(resolve => setTimeout(resolve, 500));
      const result = await updateData(updates);
      
      setOperationState('confirming');
      await new Promise(resolve => setTimeout(resolve, 300));
      
      setData(result);
      setOperationState('success');
      
      setTimeout(() => setOperationState('idle'), 1000);
    } catch (error) {
      setOperationState('error');
      
      setTimeout(() => setOperationState('idle'), 2000);
    }
  };
  
  return (
    <div className={`data-container state-${operationState}`}>
      <div className="value-display">
        {optimisticData.value}
      </div>
      
      <div className="state-indicator">
        {operationState === 'optimistic' && (
          <span className="optimistic">⏳ 更新中...</span>
        )}
        {operationState === 'confirming' && (
          <span className="confirming">✓ 确认中...</span>
        )}
        {operationState === 'success' && (
          <span className="success">✓ 已保存</span>
        )}
        {operationState === 'error' && (
          <span className="error">✗ 已回滚</span>
        )}
      </div>
      
      <button onClick={() => handleUpdate({ value: optimisticData.value + 1 })}>
        增加
      </button>
    </div>
  );
}

/* CSS */
/*
.state-optimistic {
  opacity: 0.7;
  background-color: #fff3cd;
}

.state-confirming {
  animation: pulse 0.5s;
}

.state-success {
  background-color: #d4edda;
}

.state-error {
  animation: shake 0.5s;
  background-color: #f8d7da;
}

@keyframes pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.05); }
}
*/

5.2 离线支持

jsx
'use client';

import { useOptimistic, useState, useEffect } from 'react';

export default function OfflineSupport({ initialData }) {
  const [data, setData] = useState(initialData);
  const [isOnline, setIsOnline] = useState(true);
  const [pendingUpdates, setPendingUpdates] = useState([]);
  
  const [optimisticData, setOptimisticData] = useOptimistic(
    data,
    (current, updates) => ({ ...current, ...updates })
  );
  
  // 监听网络状态
  useEffect(() => {
    const handleOnline = () => {
      setIsOnline(true);
      // 重新发送待处理的更新
      syncPendingUpdates();
    };
    
    const handleOffline = () => {
      setIsOnline(false);
    };
    
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  
  const syncPendingUpdates = async () => {
    for (const update of pendingUpdates) {
      try {
        const result = await updateData(update);
        setData(result);
      } catch (error) {
        console.error('同步失败:', error);
      }
    }
    setPendingUpdates([]);
  };
  
  const handleUpdate = async (updates) => {
    // 始终乐观更新
    setOptimisticData(updates);
    
    if (!isOnline) {
      // 离线时保存到待处理列表
      setPendingUpdates(prev => [...prev, updates]);
      toast.info('离线状态,更新将在联网后同步');
      return;
    }
    
    try {
      const result = await updateData(updates);
      setData(result);
    } catch (error) {
      toast.error('更新失败');
    }
  };
  
  return (
    <div>
      {!isOnline && (
        <div className="offline-banner">
          您当前处于离线状态,更新将在联网后自动同步
          {pendingUpdates.length > 0 && (
            <span>(待同步: {pendingUpdates.length})</span>
          )}
        </div>
      )}
      
      <input
        value={optimisticData.value}
        onChange={(e) => handleUpdate({ value: e.target.value })}
      />
    </div>
  );
}

注意事项

1. 不要手动回滚

jsx
// ❌ 错误:手动回滚
const handleLike = async () => {
  setOptimisticLikes(likes + 1);
  
  try {
    await likePost(postId);
  } catch (error) {
    // 错误!不需要手动回滚
    setOptimisticLikes(likes);
  }
};

// ✅ 正确:让useOptimistic自动回滚
const handleLike = async () => {
  setOptimisticLikes(likes + 1);
  
  try {
    const newLikes = await likePost(postId);
    setLikes(newLikes);  // 只需更新成功状态
  } catch (error) {
    // 自动回滚,只需处理错误提示
    alert('操作失败');
  }
};

2. 提供清晰的错误信息

jsx
// ✅ 清晰的错误消息
try {
  await updateTodo(id);
} catch (error) {
  if (error.status === 404) {
    setError('待办事项不存在');
  } else if (error.status === 403) {
    setError('没有权限修改');
  } else {
    setError('更新失败,请重试');
  }
}

3. 考虑网络状况

jsx
// ✅ 检测网络状态
const handleLike = async () => {
  if (!navigator.onLine) {
    alert('网络连接已断开');
    return;
  }
  
  setOptimisticLikes(likes + 1);
  
  try {
    const newLikes = await likePost(postId);
    setLikes(newLikes);
  } catch (error) {
    alert('点赞失败');
  }
};

4. 避免回滚闪烁

jsx
// ✅ 添加最小延迟避免闪烁
const handleUpdate = async (updates) => {
  setOptimisticData(updates);
  
  try {
    // 确保至少300ms的延迟,避免瞬间回滚
    const [result] = await Promise.all([
      updateData(updates),
      new Promise(resolve => setTimeout(resolve, 300))
    ]);
    
    setData(result);
  } catch (error) {
    // 回滚
  }
};

5. 保持状态一致性

jsx
// ✅ 确保状态同步
const handleUpdate = async (updates) => {
  setOptimisticData(updates);
  
  try {
    const result = await updateData(updates);
    
    // 使用服务器返回的数据,而不是本地数据
    setData(result);
  } catch (error) {
    // 自动回滚
  }
};

常见问题

Q1: useOptimistic如何知道何时回滚?

A: 当异步操作完成后,如果没有更新实际状态(likes),useOptimistic会自动回滚到实际状态的值。

Q2: 可以阻止自动回滚吗?

A: 不能也不应该。自动回滚是useOptimistic的核心特性,确保UI始终反映真实状态。

Q3: 如何处理多个并发的乐观更新?

A: 每个useOptimistic独立管理,互不干扰。多个更新可以同时进行,各自处理成功或失败。

Q4: 回滚会触发重新渲染吗?

A: 会。回滚时optimistic值变化,组件会重新渲染以显示正确的状态。

Q5: 如何避免频繁回滚?

A:

  1. 在发送请求前进行客户端验证
  2. 改善网络连接质量
  3. 实现合理的重试机制
  4. 使用防抖/节流减少请求频率

Q6: 回滚时如何保留用户输入?

A: 使用受控组件并维护独立的输入状态:

jsx
const [inputValue, setInputValue] = useState('');
const [savedValue, setSavedValue] = useState('');

const [optimisticValue, setOptimisticValue] = useOptimistic(
  savedValue,
  (_, newValue) => newValue
);

const handleSave = async () => {
  setOptimisticValue(inputValue);
  
  try {
    await saveValue(inputValue);
    setSavedValue(inputValue);
  } catch (error) {
    // 回滚,但inputValue保持不变
  }
};

总结

错误回滚要点

✅ useOptimistic自动回滚
✅ 无需手动处理回滚
✅ 提供清晰错误消息
✅ 实现重试机制
✅ 处理部分失败
✅ 显示待确认状态
✅ 考虑网络状况
✅ 优化视觉反馈
✅ 支持离线操作

最佳实践

1. 信任自动回滚机制
2. 只处理错误提示和用户反馈
3. 实现合理的重试策略
4. 提供手动重试选项
5. 显示清晰的操作状态
6. 处理批量操作和部分失败
7. 提供优雅的错误UI
8. 考虑离线场景
9. 添加操作日志和监控
10. 测试各种失败场景

用户体验建议

✅ 使用动画平滑过渡
✅ 提供即时的视觉反馈
✅ 显示操作进度
✅ 清晰的成功/失败提示
✅ 合理的重试选项
✅ 离线状态提示
✅ 保留用户输入
✅ 避免闪烁和跳动

测试清单

□ 测试网络失败场景
□ 测试服务器错误响应
□ 测试并发更新
□ 测试部分成功场景
□ 测试离线/在线切换
□ 测试重试机制
□ 测试回滚动画
□ 测试错误消息显示
□ 测试状态一致性
□ 测试性能影响

完善的错误回滚机制让乐观更新既快速又可靠!