You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Apollo Client 2.0中带节流的双向服务器同步实现咨询

嘿,我来帮你梳理下在Apollo Client 2.0里实现这个带节流的双向服务器同步功能的具体方案,毕竟我之前也在项目里处理过类似的场景,踩过一些坑~

核心思路

Apollo Client 2.0自带了强大的缓存管理能力,完全可以替代原来Redux+Saga的状态管理逻辑。我们可以拆解成两个核心场景来实现:

  1. 本地即时更新 + 节流同步服务器:编辑时先更新Apollo缓存让UI立刻响应,再用节流函数延迟发送mutation到服务器
  2. 订阅推送时跳过编辑中项:通过本地状态标记正在编辑的TODO ID,收到订阅更新时判断是否需要同步到UI

1. 本地即时更新 + 节流同步服务器

实现步骤

  • 用writeFragment直接修改Apollo缓存中的TODO数据,触发UI即时更新
  • 用节流函数(比如Lodash的_.throttle)包装GraphQL mutation,避免每次按键都发请求
  • 处理同步失败的回滚逻辑,确保本地缓存和服务器数据一致

示例代码

import { useApolloClient, useMutation, useMemo } from '@apollo/client';
import { throttle } from 'lodash';
import { useState } from 'react';

// 定义mutation和fragment
const UPDATE_TODO = gql`
  mutation UpdateTodo($id: ID!, $text: String!) {
    updateTodo(id: $id, text: $text) {
      id
      text
    }
  }
`;

const TODO_FRAGMENT = gql`
  fragment TodoFragment on Todo {
    id
    text
  }
`;

function TodoItem({ todo }) {
  const client = useApolloClient();
  const [updateTodoMutation] = useMutation(UPDATE_TODO);
  const [editingTodoId, setEditingTodoId] = useState(null);

  // 创建节流后的mutation函数,300ms内只执行最后一次
  const throttledUpdateTodo = useMemo(() => throttle(async (id, text) => {
    try {
      await updateTodoMutation({ variables: { id, text } });
    } catch (err) {
      // 同步失败:回滚本地缓存,重新从服务器拉取最新数据
      const { todo: serverTodo } = await client.query({
        query: gql`
          query GetTodo($id: ID!) {
            todo(id: $id) {
              ...TodoFragment
            }
          }
          ${TODO_FRAGMENT}
        `,
        variables: { id }
      });
      client.writeFragment({
        id: `Todo:${id}`, // Apollo默认缓存ID格式:typename:id
        fragment: TODO_FRAGMENT,
        data: serverTodo
      });
    }
  }, 300), [updateTodoMutation, client]);

  const handleTextChange = (e) => {
    const newText = e.target.value;
    // 第一步:更新本地缓存,UI立刻刷新
    client.writeFragment({
      id: `Todo:${todo.id}`,
      fragment: TODO_FRAGMENT,
      data: { ...todo, text: newText }
    });
    // 第二步:调用节流后的mutation同步服务器
    throttledUpdateTodo(todo.id, newText);
  };

  return (
    <div>
      <input
        type="text"
        value={todo.text}
        onChange={handleTextChange}
        onFocus={() => setEditingTodoId(todo.id)}
        onBlur={() => {
          setEditingTodoId(null);
          // 失去焦点时立刻触发同步,避免用户离开后数据未提交
          throttledUpdateTodo.flush();
        }}
      />
    </div>
  );
}

2. 订阅推送时跳过编辑中项

实现步骤

  • 在组件(或全局状态)中维护editingTodoId,标记当前正在编辑的TODO ID
  • 收到订阅的更新数据后,判断推送的TODO ID是否等于editingTodoId,若相等则跳过缓存更新

示例代码

import { useSubscription, useApolloClient, useEffect } from '@apollo/client';
import { useState } from 'react';

const TODO_UPDATED = gql`
  subscription TodoUpdated {
    todoUpdated {
      ...TodoFragment
    }
  }
  ${TODO_FRAGMENT}
`;

function TodoList() {
  const client = useApolloClient();
  const [editingTodoId, setEditingTodoId] = useState(null);
  const { data } = useSubscription(TODO_UPDATED);

  useEffect(() => {
    if (data?.todoUpdated) {
      const updatedTodo = data.todoUpdated;
      // 只更新非编辑状态的TODO
      if (updatedTodo.id !== editingTodoId) {
        client.writeFragment({
          id: `Todo:${updatedTodo.id}`,
          fragment: TODO_FRAGMENT,
          data: updatedTodo
        });
      }
    }
  }, [data, client, editingTodoId]);

  // 传递editingTodoId给子组件TodoItem,统一维护状态
  return (
    <div>
      {todos.map(todo => (
        <TodoItem 
          key={todo.id} 
          todo={todo} 
          editingTodoId={editingTodoId}
          setEditingTodoId={setEditingTodoId}
        />
      ))}
    </div>
  );
}

额外注意事项
  • 缓存ID规则:Apollo默认用typename:id作为缓存key,确保你的TODO类型定义中包含id字段,或者自定义dataIdFromObject来生成唯一ID
  • 节流时间调整:根据业务场景选择合适的节流时长(300-500ms比较通用),平衡请求频率和用户体验
  • 订阅范围优化:如果是大型列表,建议使用针对单个TODO的订阅(传递id参数),而非全局订阅,减少不必要的数据传输

内容的提问来源于stack exchange,提问作者Alexander Kachkaev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 10:56:03