Apollo客户端模式Skill apollo-client-patterns

该技能用于在React应用中实现Apollo Client的GraphQL模式,包括查询、突变、缓存管理、本地状态处理等。关键词:Apollo Client, GraphQL, React, 缓存, 状态管理, 前端开发。

前端开发 0 次安装 0 次浏览 更新于 3/25/2026

name: apollo-client-patterns user-invocable: false description: 用于在React应用中实现Apollo Client模式,包括查询、突变、缓存管理和本地状态。 allowed-tools:

  • Read
  • Write
  • Edit
  • Grep
  • Glob
  • Bash

Apollo客户端模式

掌握Apollo Client,以构建高效的GraphQL应用,包括正确的查询管理、缓存策略和状态处理。

概述

Apollo Client是一个全面的JavaScript状态管理库,使您能够使用GraphQL管理本地和远程数据。它与React无缝集成,并提供强大的缓存机制。

安装和设置

安装Apollo Client

# 安装Apollo Client和依赖项
npm install @apollo/client graphql

# 对于React应用
npm install @apollo/client graphql react

# 额外包
npm install graphql-tag @apollo/client/link/error

基本配置

// src/apollo/client.js
import {
  ApolloClient,
  InMemoryCache,
  createHttpLink,
  from
} from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';

const httpLink = createHttpLink({
  uri: process.env.REACT_APP_GRAPHQL_URI || 'http://localhost:4000/graphql',
});

const authLink = setContext((_, { headers }) => {
  const token = localStorage.getItem('authToken');
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : '',
    }
  };
});

const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, locations, path }) =>
      console.error(
        `[GraphQL错误]: 消息: ${message}, 位置: ${locations}, 路径: ${path}`
      )
    );
  }
  if (networkError) {
    console.error(`[网络错误]: ${networkError}`);
  }
});

const client = new ApolloClient({
  link: from([errorLink, authLink, httpLink]),
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          posts: {
            merge(existing, incoming) {
              return incoming;
            }
          }
        }
      }
    }
  }),
  defaultOptions: {
    watchQuery: {
      fetchPolicy: 'cache-and-network',
      errorPolicy: 'all',
    },
    query: {
      fetchPolicy: 'network-only',
      errorPolicy: 'all',
    },
  },
});

export default client;

提供者设置

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { ApolloProvider } from '@apollo/client';
import client from './apollo/client';
import App from './App';

ReactDOM.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>,
  document.getElementById('root')
);

核心模式

1. 基本查询

// src/graphql/queries.js
import { gql } from '@apollo/client';

export const GET_POSTS = gql`
  query GetPosts($limit: Int, $offset: Int) {
    posts(limit: $limit, offset: $offset) {
      id
      title
      body
      author {
        id
        name
        avatar
      }
      createdAt
    }
  }
`;

export const GET_POST = gql`
  query GetPost($id: ID!) {
    post(id: $id) {
      id
      title
      body
      author {
        id
        name
      }
      comments {
        id
        body
        author {
          id
          name
        }
      }
    }
  }
`;

// src/components/PostsList.js
import React from 'react';
import { useQuery } from '@apollo/client';
import { GET_POSTS } from '../graphql/queries';

function PostsList() {
  const { loading, error, data, refetch, fetchMore } = useQuery(GET_POSTS, {
    variables: { limit: 10, offset: 0 },
    notifyOnNetworkStatusChange: true,
  });

  if (loading) return <p>加载中...</p>;
  if (error) return <p>错误: {error.message}</p>;

  return (
    <div>
      <button onClick={() => refetch()}>刷新</button>

      {data.posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
          <span>作者: {post.author.name}</span>
        </article>
      ))}

      <button
        onClick={() =>
          fetchMore({
            variables: { offset: data.posts.length },
            updateQuery: (prev, { fetchMoreResult }) => {
              if (!fetchMoreResult) return prev;
              return {
                posts: [...prev.posts, ...fetchMoreResult.posts]
              };
            }
          })
        }
      >
        加载更多
      </button>
    </div>
  );
}

export default PostsList;

2. 突变

// src/graphql/mutations.js
import { gql } from '@apollo/client';

export const CREATE_POST = gql`
  mutation CreatePost($input: CreatePostInput!) {
    createPost(input: $input) {
      id
      title
      body
      author {
        id
        name
      }
      createdAt
    }
  }
`;

export const UPDATE_POST = gql`
  mutation UpdatePost($id: ID!, $input: UpdatePostInput!) {
    updatePost(id: $id, input: $input) {
      id
      title
      body
    }
  }
`;

export const DELETE_POST = gql`
  mutation DeletePost($id: ID!) {
    deletePost(id: $id) {
      id
    }
  }
`;

// src/components/CreatePost.js
import React, { useState } from 'react';
import { useMutation } from '@apollo/client';
import { CREATE_POST } from '../graphql/mutations';
import { GET_POSTS } from '../graphql/queries';

function CreatePost() {
  const [title, setTitle] = useState('');
  const [body, setBody] = useState('');

  const [createPost, { loading, error }] = useMutation(CREATE_POST, {
    update(cache, { data: { createPost } }) {
      const { posts } = cache.readQuery({ query: GET_POSTS });
      cache.writeQuery({
        query: GET_POSTS,
        data: { posts: [createPost, ...posts] }
      });
    },
    onCompleted: () => {
      setTitle('');
      setBody('');
    },
    onError: (error) => {
      console.error('创建帖子错误:', error);
    }
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    createPost({
      variables: {
        input: { title, body }
      }
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={title}
        onChange={e => setTitle(e.target.value)}
        placeholder="标题"
        disabled={loading}
      />
      <textarea
        value={body}
        onChange={e => setBody(e.target.value)}
        placeholder="正文"
        disabled={loading}
      />
      <button type="submit" disabled={loading}>
        {loading ? '创建中...' : '创建帖子'}
      </button>
      {error && <p>错误: {error.message}</p>}
    </form>
  );
}

export default CreatePost;

3. 缓存管理

// src/apollo/cache.js
import { InMemoryCache, makeVar } from '@apollo/client';

// 响应式变量
export const cartItemsVar = makeVar([]);
export const isLoggedInVar = makeVar(!!localStorage.getItem('authToken'));

export const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        cartItems: {
          read() {
            return cartItemsVar();
          }
        },
        isLoggedIn: {
          read() {
            return isLoggedInVar();
          }
        },
        // 分页与字段策略
        posts: {
          keyArgs: false,
          merge(existing = [], incoming, { args }) {
            const merged = existing ? existing.slice(0) : [];
            const offset = args?.offset || 0;

            for (let i = 0; i < incoming.length; i++) {
              merged[offset + i] = incoming[i];
            }

            return merged;
          }
        }
      }
    },
    Post: {
      fields: {
        // 计算字段
        isLiked: {
          read(_, { readField }) {
            const likes = readField('likes');
            const currentUserId = localStorage.getItem('userId');
            return likes?.some(like => like.userId === currentUserId);
          }
        }
      }
    }
  }
});

// 缓存操作助手
export function addToCart(item) {
  const currentCart = cartItemsVar();
  cartItemsVar([...currentCart, item]);
}

export function removeFromCart(itemId) {
  const currentCart = cartItemsVar();
  cartItemsVar(currentCart.filter(item => item.id !== itemId));
}

// 手动缓存更新
export function updatePostInCache(client, postId, updates) {
  const post = client.readFragment({
    id: `Post:${postId}`,
    fragment: gql`
      fragment PostUpdate on Post {
        id
        title
        body
      }
    `
  });

  if (post) {
    client.writeFragment({
      id: `Post:${postId}`,
      fragment: gql`
        fragment PostUpdate on Post {
          id
          title
          body
        }
      `,
      data: {
        ...post,
        ...updates
      }
    });
  }
}

4. 乐观更新

// src/components/LikeButton.js
import React from 'react';
import { useMutation } from '@apollo/client';
import { gql } from '@apollo/client';

const LIKE_POST = gql`
  mutation LikePost($postId: ID!) {
    likePost(postId: $postId) {
      id
      likesCount
      isLiked
    }
  }
`;

function LikeButton({ post }) {
  const [likePost] = useMutation(LIKE_POST, {
    variables: { postId: post.id },
    optimisticResponse: {
      __typename: 'Mutation',
      likePost: {
        __typename: 'Post',
        id: post.id,
        likesCount: post.likesCount + 1,
        isLiked: true,
      }
    },
    update(cache, { data: { likePost } }) {
      cache.modify({
        id: cache.identify(post),
        fields: {
          likesCount() {
            return likePost.likesCount;
          },
          isLiked() {
            return likePost.isLiked;
          }
        }
      });
    }
  });

  return (
    <button onClick={() => likePost()}>
      {post.isLiked ? '取消点赞' : '点赞'} ({post.likesCount})
    </button>
  );
}

export default LikeButton;

5. 订阅

// src/graphql/subscriptions.js
import { gql } from '@apollo/client';

export const POST_CREATED = gql`
  subscription OnPostCreated {
    postCreated {
      id
      title
      body
      author {
        id
        name
      }
      createdAt
    }
  }
`;

// src/components/RealtimePosts.js
import React from 'react';
import { useQuery, useSubscription } from '@apollo/client';
import { GET_POSTS } from '../graphql/queries';
import { POST_CREATED } from '../graphql/subscriptions';

function RealtimePosts() {
  const { data, loading } = useQuery(GET_POSTS);

  useSubscription(POST_CREATED, {
    onSubscriptionData: ({ client, subscriptionData }) => {
      const newPost = subscriptionData.data.postCreated;

      client.cache.modify({
        fields: {
          posts(existingPosts = []) {
            const newPostRef = client.cache.writeFragment({
              data: newPost,
              fragment: gql`
                fragment NewPost on Post {
                  id
                  title
                  body
                  author {
                    id
                    name
                  }
                }
              `
            });
            return [newPostRef, ...existingPosts];
          }
        }
      });
    }
  });

  if (loading) return <p>加载中...</p>;

  return (
    <div>
      {data.posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </article>
      ))}
    </div>
  );
}

export default RealtimePosts;

6. 懒查询

// src/components/SearchPosts.js
import React, { useState } from 'react';
import { useLazyQuery } from '@apollo/client';
import { gql } from '@apollo/client';

const SEARCH_POSTS = gql`
  query SearchPosts($query: String!) {
    searchPosts(query: $query) {
      id
      title
      excerpt
    }
  }
`;

function SearchPosts() {
  const [searchTerm, setSearchTerm] = useState('');
  const [searchPosts, { loading, data, error, called }] = useLazyQuery(
    SEARCH_POSTS,
    {
      fetchPolicy: 'network-only'
    }
  );

  const handleSearch = (e) => {
    e.preventDefault();
    if (searchTerm.trim()) {
      searchPosts({ variables: { query: searchTerm } });
    }
  };

  return (
    <div>
      <form onSubmit={handleSearch}>
        <input
          value={searchTerm}
          onChange={e => setSearchTerm(e.target.value)}
          placeholder="搜索帖子..."
        />
        <button type="submit">搜索</button>
      </form>

      {loading && <p>搜索中...</p>}
      {error && <p>错误: {error.message}</p>}

      {called && data && (
        <ul>
          {data.searchPosts.map(post => (
            <li key={post.id}>
              <h3>{post.title}</h3>
              <p>{post.excerpt}</p>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default SearchPosts;

7. 错误处理

// src/components/PostWithErrorHandling.js
import React from 'react';
import { useQuery } from '@apollo/client';
import { GET_POST } from '../graphql/queries';

function PostWithErrorHandling({ postId }) {
  const { loading, error, data } = useQuery(GET_POST, {
    variables: { id: postId },
    errorPolicy: 'all', // 返回部分数据和错误
    onError: (error) => {
      // 自定义错误处理
      if (error.networkError) {
        console.error('网络错误:', error.networkError);
      }
      if (error.graphQLErrors) {
        error.graphQLErrors.forEach(({ message, extensions }) => {
          if (extensions.code === 'UNAUTHENTICATED') {
            // 重定向到登录
            window.location.href = '/login';
          }
        });
      }
    }
  });

  if (loading) return <p>加载中...</p>;

  if (error && !data) {
    return (
      <div className="error">
        <h3>出了点问题</h3>
        <p>{error.message}</p>
        <button onClick={() => window.location.reload()}>
          重试
        </button>
      </div>
    );
  }

  // 部分数据带错误
  if (error && data) {
    console.warn('部分数据带错误:', error);
  }

  return (
    <article>
      <h1>{data.post.title}</h1>
      <p>{data.post.body}</p>
    </article>
  );
}

export default PostWithErrorHandling;

8. 分页模式

// src/components/PaginatedPosts.js
import React from 'react';
import { useQuery } from '@apollo/client';
import { gql } from '@apollo/client';

const GET_PAGINATED_POSTS = gql`
  query GetPaginatedPosts($cursor: String, $limit: Int!) {
    posts(cursor: $cursor, limit: $limit) {
      edges {
        node {
          id
          title
          body
        }
        cursor
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
`;

function PaginatedPosts() {
  const { data, loading, fetchMore, networkStatus } = useQuery(
    GET_PAGINATED_POSTS,
    {
      variables: { limit: 10 },
      notifyOnNetworkStatusChange: true,
    }
  );

  const loadMore = () => {
    fetchMore({
      variables: {
        cursor: data.posts.pageInfo.endCursor
      }
    });
  };

  if (loading && networkStatus !== 3) return <p>加载中...</p>;

  return (
    <div>
      {data.posts.edges.map(({ node }) => (
        <article key={node.id}>
          <h2>{node.title}</h2>
          <p>{node.body}</p>
        </article>
      ))}

      {data.posts.pageInfo.hasNextPage && (
        <button onClick={loadMore} disabled={networkStatus === 3}>
          {networkStatus === 3 ? '加载中...' : '加载更多'}
        </button>
      )}
    </div>
  );
}

export default PaginatedPosts;

9. 本地状态管理

// src/graphql/local.js
import { gql, makeVar } from '@apollo/client';

// 响应式变量
export const themeVar = makeVar('light');
export const sidebarOpenVar = makeVar(false);

// 仅本地字段
export const LOCAL_STATE = gql`
  query GetLocalState {
    theme @client
    sidebarOpen @client
  }
`;

// 本地状态的类型策略
export const localStateTypePolicies = {
  Query: {
    fields: {
      theme: {
        read() {
          return themeVar();
        }
      },
      sidebarOpen: {
        read() {
          return sidebarOpenVar();
        }
      }
    }
  }
};

// src/components/ThemeToggle.js
import React from 'react';
import { useQuery } from '@apollo/client';
import { LOCAL_STATE, themeVar } from '../graphql/local';

function ThemeToggle() {
  const { data } = useQuery(LOCAL_STATE);

  const toggleTheme = () => {
    const newTheme = data.theme === 'light' ? 'dark' : 'light';
    themeVar(newTheme);
    localStorage.setItem('theme', newTheme);
  };

  return (
    <button onClick={toggleTheme}>
      当前主题: {data.theme}
    </button>
  );
}

export default ThemeToggle;

10. 自定义钩子

// src/hooks/usePosts.js
import { useQuery, useMutation } from '@apollo/client';
import { GET_POSTS, GET_POST } from '../graphql/queries';
import { CREATE_POST, UPDATE_POST, DELETE_POST } from '../graphql/mutations';

export function usePosts() {
  const { data, loading, error, refetch } = useQuery(GET_POSTS);

  return {
    posts: data?.posts || [],
    loading,
    error,
    refetch
  };
}

export function usePost(id) {
  const { data, loading, error } = useQuery(GET_POST, {
    variables: { id },
    skip: !id
  });

  return {
    post: data?.post,
    loading,
    error
  };
}

export function useCreatePost() {
  const [createPost, { loading, error }] = useMutation(CREATE_POST, {
    update(cache, { data: { createPost } }) {
      cache.modify({
        fields: {
          posts(existingPosts = []) {
            const newPostRef = cache.writeFragment({
              data: createPost,
              fragment: gql`
                fragment NewPost on Post {
                  id
                  title
                  body
                }
              `
            });
            return [newPostRef, ...existingPosts];
          }
        }
      });
    }
  });

  return { createPost, loading, error };
}

// 使用
function MyComponent() {
  const { posts, loading } = usePosts();
  const { createPost } = useCreatePost();

  // ...
}

最佳实践

  1. 使用片段 - 跨查询共享字段选择
  2. 实现错误边界 - 优雅地处理错误
  3. 优化缓存配置 - 正确配置类型策略
  4. 使用乐观更新 - 提高感知性能
  5. 实现适当的加载状态 - 在操作期间显示反馈
  6. 避免过度获取 - 仅请求所需字段
  7. 利用自动缓存 - 让Apollo处理缓存
  8. 使用响应式变量 - 高效管理本地状态
  9. 实现分页 - 正确处理大数据集
  10. 监控网络状态 - 准确跟踪查询状态

常见陷阱

  1. 缓存不一致 - 突变后未更新缓存
  2. 过度获取数据 - 请求不必要的字段
  3. 缺少错误处理 - 未处理网络/GraphQL错误
  4. 轮询滥用 - 过度轮询导致性能问题
  5. 未使用片段 - 重复字段选择
  6. 不当缓存规范化 - 缺失或错误的缓存ID
  7. 内存泄漏 - 未清理订阅
  8. 陈旧数据 - 使用错误的获取策略
  9. 缺少加载状态 - 用户体验差
  10. 认证令牌问题 - 未刷新过期令牌

使用场景

  • 使用GraphQL API构建React应用
  • 管理复杂的应用状态
  • 实现实时功能
  • 创建数据驱动UI
  • 使用React Native构建移动应用
  • 开发管理仪表板
  • 创建协作应用
  • 实现离线优先功能
  • 构建电子商务平台
  • 开发社交媒体应用

资源