Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Apollo Client - kompleksowa integracja GraphQL z React

Apollo Client to potężna biblioteka do zarządzania stanem i komunikacji z serwerami GraphQL w aplikacjach React. Oferuje zaawansowane funkcje cache'owania, optymistyczne aktualizacje, obsługę subskrypcji i wiele więcej.

Instalacja i podstawowa konfiguracja

Instalacja

1npm install @apollo/client graphql
2# Lub z dodatkowym toolingiem
3npm install @apollo/client graphql apollo-link-error apollo-upload-client

Podstawowa konfiguracja Apollo Client

1// apolloClient.js
2import { ApolloClient, InMemoryCache, createHttpLink, from } from '@apollo/client';
3import { setContext } from '@apollo/client/link/context';
4import { onError } from '@apollo/client/link/error';
5
6// Link HTTP do komunikacji z serwerem GraphQL
7const httpLink = createHttpLink({
8  uri: process.env.REACT_APP_GRAPHQL_ENDPOINT || 'http://localhost:4000/graphql',
9  credentials: 'include' // Dla cookies/session
10});
11
12// Link do obsługi autoryzacji
13const authLink = setContext((_, { headers }) => {
14  const token = localStorage.getItem('authToken');
15  
16  return {
17    headers: {
18      ...headers,
19      authorization: token ? `Bearer ${token}` : '',
20      'x-api-version': '1.0',
21      'x-client-name': 'react-app'
22    }
23  };
24});
25
26// Link do obsługi błędów
27const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
28  if (graphQLErrors) {
29    graphQLErrors.forEach(({ message, locations, path, extensions }) => {
30      console.error(
31        `GraphQL error: Message: ${message}, Location: ${locations}, Path: ${path}`
32      );
33      
34      // Obsługa specyficznych błędów
35      if (extensions?.code === 'UNAUTHENTICATED') {
36        localStorage.removeItem('authToken');
37        window.location.href = '/login';
38      }
39    });
40  }
41  
42  if (networkError) {
43    console.error(`Network error: ${networkError}`);
44    
45    // Retry logic dla błędów sieciowych
46    if (networkError.statusCode === 500) {
47      return forward(operation);
48    }
49  }
50});
51
52// Konfiguracja cache
53const cache = new InMemoryCache({
54  typePolicies: {
55    User: {
56      fields: {
57        // Konfiguracja cache dla pól użytkownika
58        posts: {
59          merge(existing = [], incoming) {
60            return [...existing, ...incoming];
61          }
62        }
63      }
64    },
65    Post: {
66      fields: {
67        comments: {
68          merge(existing = [], incoming) {
69            return incoming;
70          }
71        }
72      }
73    }
74  }
75});
76
77// Główny klient Apollo
78const apolloClient = new ApolloClient({
79  link: from([errorLink, authLink, httpLink]),
80  cache,
81  defaultOptions: {
82    watchQuery: {
83      errorPolicy: 'all',
84      notifyOnNetworkStatusChange: true
85    },
86    query: {
87      errorPolicy: 'all'
88    }
89  },
90  connectToDevTools: process.env.NODE_ENV === 'development'
91});
92
93export default apolloClient;

Integracja z aplikacją React

1// App.js
2import React from 'react';
3import { ApolloProvider } from '@apollo/client';
4import apolloClient from './apolloClient';
5import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
6import Dashboard from './components/Dashboard';
7import UserProfile from './components/UserProfile';
8
9function App() {
10  return (
11    <ApolloProvider client={apolloClient}>
12      <Router>
13        <div className="App">
14          <Routes>
15            <Route path="/" element={<Dashboard />} />
16            <Route path="/profile/:id" element={<UserProfile />} />
17          </Routes>
18        </div>
19      </Router>
20    </ApolloProvider>
21  );
22}
23
24export default App;

Wykonywanie zapytań (Queries)

Podstawowe zapytania z useQuery

1// Definicja zapytania GraphQL
2import { gql, useQuery } from '@apollo/client';
3
4const GET_USERS = gql`
5  query GetUsers($limit: Int, $offset: Int) {
6    users(limit: $limit, offset: $offset) {
7      id
8      name
9      email
10      avatar
11      posts {
12        id
13        title
14        createdAt
15      }
16    }
17  }
18`;
19
20function UsersList() {
21  const { data, loading, error, refetch, fetchMore } = useQuery(GET_USERS, {
22    variables: { limit: 10, offset: 0 },
23    notifyOnNetworkStatusChange: true,
24    errorPolicy: 'all'
25  });
26
27  if (loading) return <div className="loading">Ładowanie użytkowników...</div>;
28  if (error) return <div className="error">Błąd: {error.message}</div>;
29
30  const loadMoreUsers = () => {
31    fetchMore({
32      variables: {
33        offset: data.users.length
34      },
35      updateQuery: (prev, { fetchMoreResult }) => {
36        if (!fetchMoreResult) return prev;
37        
38        return {
39          ...prev,
40          users: [...prev.users, ...fetchMoreResult.users]
41        };
42      }
43    });
44  };
45
46  return (
47    <div>
48      <h2>Lista użytkowników</h2>
49      <button onClick={() => refetch()}>Odśwież</button>
50      
51      <div className="users-grid">
52        {data.users.map(user => (
53          <div key={user.id} className="user-card">
54            <img src={user.avatar} alt={user.name} />
55            <h3>{user.name}</h3>
56            <p>{user.email}</p>
57            <p>Posty: {user.posts.length}</p>
58          </div>
59        ))}
60      </div>
61      
62      <button onClick={loadMoreUsers}>Załaduj więcej</button>
63    </div>
64  );
65}

Lazy queries - zapytania na żądanie

1import { gql, useLazyQuery } from '@apollo/client';
2import { useState } from 'react';
3
4const SEARCH_USERS = gql`
5  query SearchUsers($query: String!) {
6    searchUsers(query: $query) {
7      id
8      name
9      email
10      avatar
11    }
12  }
13`;
14
15function UserSearch() {
16  const [searchTerm, setSearchTerm] = useState('');
17  const [searchUsers, { data, loading, error }] = useLazyQuery(SEARCH_USERS);
18
19  const handleSearch = (e) => {
20    e.preventDefault();
21    if (searchTerm.trim()) {
22      searchUsers({
23        variables: { query: searchTerm }
24      });
25    }
26  };
27
28  return (
29    <div>
30      <form onSubmit={handleSearch}>
31        <input
32          type="text"
33          value={searchTerm}
34          onChange={(e) => setSearchTerm(e.target.value)}
35          />
36        <button type="submit">Szukaj</button>
37      </form>
38
39      {loading && <div>Wyszukiwanie...</div>}
40      {error && <div>Błąd wyszukiwania: {error.message}</div>}
41      
42      {data?.searchUsers && (
43        <div>
44          <h3>Wyniki wyszukiwania:</h3>
45          {data.searchUsers.map(user => (
46            <div key={user.id}>{user.name} - {user.email}</div>
47          ))}
48        </div>
49      )}
50    </div>
51  );
52}

Mutacje (Mutations)

Podstawowe mutacje z useMutation

1import { gql, useMutation } from '@apollo/client';
2import { useState } from 'react';
3
4const CREATE_POST = gql`
5  mutation CreatePost($input: CreatePostInput!) {
6    createPost(input: $input) {
7      id
8      title
9      content
10      author {
11        id
12        name
13      }
14      createdAt
15    }
16  }
17`;
18
19const GET_POSTS = gql`
20  query GetPosts {
21    posts {
22      id
23      title
24      content
25      author {
26        id
27        name
28      }
29      createdAt
30    }
31  }
32`;
33
34function CreatePostForm() {
35  const [title, setTitle] = useState('');
36  const [content, setContent] = useState('');
37  
38  const [createPost, { loading, error }] = useMutation(CREATE_POST, {
39    // Aktualizacja cache po utworzeniu posta
40    update(cache, { data: { createPost } }) {
41      const existingPosts = cache.readQuery({ query: GET_POSTS });
42      
43      cache.writeQuery({
44        query: GET_POSTS,
45        data: {
46          posts: [createPost, ...existingPosts.posts]
47        }
48      });
49    },
50    
51    // Optymistyczna aktualizacja
52    optimisticResponse: {
53      createPost: {
54        __typename: 'Post',
55        id: 'temp-id',
56        title,
57        content,
58        author: {
59          __typename: 'User',
60          id: 'current-user-id',
61          name: 'Ty'
62        },
63        createdAt: new Date().toISOString()
64      }
65    },
66    
67    // Obsługa błędów
68    onError: (error) => {
69      console.error('Błąd tworzenia posta:', error);
70    },
71    
72    onCompleted: (data) => {
73      console.log('Post został utworzony:', data.createPost);
74      setTitle('');
75      setContent('');
76    }
77  });
78
79  const handleSubmit = async (e) => {
80    e.preventDefault();
81    
82    if (!title.trim() || !content.trim()) {
83      alert('Wypełnij wszystkie pola');
84      return;
85    }
86
87    try {
88      await createPost({
89        variables: {
90          input: { title, content }
91        }
92      });
93    } catch (err) {
94      console.error('Błąd podczas tworzenia posta:', err);
95    }
96  };
97
98  return (
99    <form onSubmit={handleSubmit}>
100      <div>
101        <label>Tytuł:</label>
102        <input
103          type="text"
104          value={title}
105          onChange={(e) => setTitle(e.target.value)}
106          disabled={loading}
107        />
108      </div>
109      
110      <div>
111        <label>Treść:</label>
112        <textarea
113          value={content}
114          onChange={(e) => setContent(e.target.value)}
115          disabled={loading}
116        />
117      </div>
118      
119      <button type="submit" disabled={loading}>
120        {loading ? 'Tworzenie...' : 'Utwórz post'}
121      </button>
122      
123      {error && <div className="error">Błąd: {error.message}</div>}
124    </form>
125  );
126}

Zaawansowane zarządzanie cache

1import { gql, useMutation, useQuery } from '@apollo/client';
2
3const DELETE_POST = gql`
4  mutation DeletePost($id: ID!) {
5    deletePost(id: $id) {
6      id
7    }
8  }
9`;
10
11const UPDATE_POST = gql`
12  mutation UpdatePost($id: ID!, $input: UpdatePostInput!) {
13    updatePost(id: $id, input: $input) {
14      id
15      title
16      content
17      updatedAt
18    }
19  }
20`;
21
22function PostManager({ postId }) {
23  const [deletePost] = useMutation(DELETE_POST, {
24    // Usuń z cache po usunięciu
25    update(cache, { data: { deletePost } }) {
26      cache.modify({
27        fields: {
28          posts(existingPosts = [], { readField }) {
29            return existingPosts.filter(
30              postRef => deletePost.id !== readField('id', postRef)
31            );
32          }
33        }
34      });
35      
36      // Usuń też z cache poszczególny post
37      cache.evict({ id: cache.identify({ __typename: 'Post', id: deletePost.id }) });
38      cache.gc();
39    }
40  });
41
42  const [updatePost] = useMutation(UPDATE_POST, {
43    // Nie potrzebujemy update - Apollo automatycznie zaktualizuje cache
44    // jeśli zwrócone dane mają to samo ID i __typename
45  });
46
47  const handleDelete = async () => {
48    if (window.confirm('Czy na pewno chcesz usunąć ten post?')) {
49      try {
50        await deletePost({
51          variables: { id: postId }
52        });
53      } catch (error) {
54        console.error('Błąd usuwania posta:', error);
55      }
56    }
57  };
58
59  const handleUpdate = async (newTitle, newContent) => {
60    try {
61      await updatePost({
62        variables: {
63          id: postId,
64          input: { title: newTitle, content: newContent }
65        }
66      });
67    } catch (error) {
68      console.error('Błąd aktualizacji posta:', error);
69    }
70  };
71
72  return (
73    <div>
74      <button onClick={handleDelete}>Usuń post</button>
75      {/* Formularz edycji */}
76    </div>
77  );
78}

Subskrypcje (Subscriptions)

Konfiguracja WebSocket dla subskrypcji

1// apolloClient.js - rozszerzenie konfiguracji
2import { split } from '@apollo/client';
3import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
4import { createClient } from 'graphql-ws';
5import { getMainDefinition } from '@apollo/client/utilities';
6
7// WebSocket link dla subskrypcji
8const wsLink = new GraphQLWsLink(
9  createClient({
10    url: process.env.REACT_APP_GRAPHQL_WS_ENDPOINT || 'ws://localhost:4000/graphql',
11    connectionParams: () => {
12      const token = localStorage.getItem('authToken');
13      return {
14        authorization: token ? `Bearer ${token}` : ''
15      };
16    },
17    on: {
18      connected: () => console.log('WebSocket connected'),
19      closed: () => console.log('WebSocket disconnected')
20    }
21  })
22);
23
24// Podział ruchu - HTTP dla queries/mutations, WebSocket dla subscriptions
25const splitLink = split(
26  ({ query }) => {
27    const definition = getMainDefinition(query);
28    return (
29      definition.kind === 'OperationDefinition' &&
30      definition.operation === 'subscription'
31    );
32  },
33  wsLink,
34  from([errorLink, authLink, httpLink])
35);
36
37// Aktualizacja klienta
38const apolloClient = new ApolloClient({
39  link: splitLink, // Użyj splitLink zamiast from([...])
40  cache,
41  // ... reszta konfiguracji
42});

Użycie subskrypcji w komponentach

1import { gql, useSubscription, useQuery } from '@apollo/client';
2
3const POST_ADDED_SUBSCRIPTION = gql`
4  subscription PostAdded {
5    postAdded {
6      id
7      title
8      content
9      author {
10        id
11        name
12      }
13      createdAt
14    }
15  }
16`;
17
18const COMMENT_ADDED_SUBSCRIPTION = gql`
19  subscription CommentAdded($postId: ID!) {
20    commentAdded(postId: $postId) {
21      id
22      content
23      author {
24        id
25        name
26      }
27      createdAt
28    }
29  }
30`;
31
32function LivePostsFeed() {
33  const { data: postsData, loading } = useQuery(GET_POSTS);
34  
35  // Subskrypcja na nowe posty
36  useSubscription(POST_ADDED_SUBSCRIPTION, {
37    onSubscriptionData: ({ subscriptionData, client }) => {
38      const newPost = subscriptionData.data.postAdded;
39      
40      // Aktualizuj cache
41      const existingPosts = client.readQuery({ query: GET_POSTS });
42      client.writeQuery({
43        query: GET_POSTS,
44        data: {
45          posts: [newPost, ...existingPosts.posts]
46        }
47      });
48      
49      // Pokazanie notyfikacji
50      showNotification(`Nowy post: ${newPost.title}`);
51    }
52  });
53
54  if (loading) return <div>Ładowanie...</div>;
55
56  return (
57    <div>
58      <h2>Na żywo - posty</h2>
59      {postsData?.posts.map(post => (
60        <LivePostCard key={post.id} post={post} />
61      ))}
62    </div>
63  );
64}
65
66function LivePostCard({ post }) {
67  // Subskrypcja na nowe komentarze dla tego posta
68  useSubscription(COMMENT_ADDED_SUBSCRIPTION, {
69    variables: { postId: post.id },
70    onSubscriptionData: ({ subscriptionData }) => {
71      const newComment = subscriptionData.data.commentAdded;
72      console.log(`Nowy komentarz w poście ${post.title}:`, newComment.content);
73    }
74  });
75
76  return (
77    <div className="post-card">
78      <h3>{post.title}</h3>
79      <p>{post.content}</p>
80      <small>Autor: {post.author.name}</small>
81    </div>
82  );
83}

Zaawansowane patterns i optymalizacje

1. Fragment composition

1// fragments.js
2import { gql } from '@apollo/client';
3
4export const USER_FRAGMENT = gql`
5  fragment UserInfo on User {
6    id
7    name
8    email
9    avatar
10  }
11`;
12
13export const POST_FRAGMENT = gql`
14  fragment PostInfo on Post {
15    id
16    title
17    content
18    createdAt
19    author {
20      ...UserInfo
21    }
22  }
23  ${USER_FRAGMENT}
24`;
25
26// Użycie fragmentów
27const GET_POSTS_WITH_FRAGMENTS = gql`
28  query GetPosts {
29    posts {
30      ...PostInfo
31    }
32  }
33  ${POST_FRAGMENT}
34`;

2. Custom hooks dla GraphQL

1// hooks/usePostOperations.js
2import { gql, useQuery, useMutation } from '@apollo/client';
3import { POST_FRAGMENT } from '../fragments';
4
5const GET_POSTS = gql`
6  query GetPosts($limit: Int, $offset: Int) {
7    posts(limit: $limit, offset: $offset) {
8      ...PostInfo
9    }
10  }
11  ${POST_FRAGMENT}
12`;
13
14const CREATE_POST = gql`
15  mutation CreatePost($input: CreatePostInput!) {
16    createPost(input: $input) {
17      ...PostInfo
18    }
19  }
20  ${POST_FRAGMENT}
21`;
22
23export function usePostOperations() {
24  const { data, loading, error, refetch, fetchMore } = useQuery(GET_POSTS, {
25    variables: { limit: 10, offset: 0 }
26  });
27
28  const [createPostMutation, { loading: creating }] = useMutation(CREATE_POST, {
29    update(cache, { data: { createPost } }) {
30      const existingPosts = cache.readQuery({ query: GET_POSTS });
31      cache.writeQuery({
32        query: GET_POSTS,
33        data: {
34          posts: [createPost, ...existingPosts.posts]
35        }
36      });
37    }
38  });
39
40  const createPost = async (postData) => {
41    try {
42      const result = await createPostMutation({
43        variables: { input: postData }
44      });
45      return { success: true, post: result.data.createPost };
46    } catch (error) {
47      return { success: false, error: error.message };
48    }
49  };
50
51  const loadMore = () => {
52    return fetchMore({
53      variables: { offset: data?.posts?.length || 0 }
54    });
55  };
56
57  return {
58    posts: data?.posts || [],
59    loading,
60    error,
61    creating,
62    createPost,
63    refetch,
64    loadMore
65  };
66}
67
68// Użycie custom hook
69function PostsPage() {
70  const { posts, loading, createPost, loadMore } = usePostOperations();
71
72  const handleCreatePost = async (formData) => {
73    const result = await createPost(formData);
74    if (result.success) {
75      console.log('Post created successfully');
76    } else {
77      console.error('Failed to create post:', result.error);
78    }
79  };
80
81  return (
82    <div>
83      <CreatePostForm onSubmit={handleCreatePost} />
84      {loading ? (
85        <div>Loading...</div>
86      ) : (
87        <PostsList posts={posts} onLoadMore={loadMore} />
88      )}
89    </div>
90  );
91}

3. Error boundaries dla GraphQL

1import React from 'react';
2import { ApolloError } from '@apollo/client';
3
4class GraphQLErrorBoundary extends React.Component {
5  constructor(props) {
6    super(props);
7    this.state = { hasError: false, error: null };
8  }
9
10  static getDerivedStateFromError(error) {
11    return { hasError: true, error };
12  }
13
14  componentDidCatch(error, errorInfo) {
15    if (error instanceof ApolloError) {
16      console.error('GraphQL Error:', error.graphQLErrors);
17      console.error('Network Error:', error.networkError);
18    }
19    
20    // Wyślij błąd do systemu monitorowania
21    this.logErrorToService(error, errorInfo);
22  }
23
24  logErrorToService(error, errorInfo) {
25    // Integracja z Sentry, LogRocket, etc.
26    console.error('Error logged:', error, errorInfo);
27  }
28
29  render() {
30    if (this.state.hasError) {
31      return (
32        <div className="error-boundary">
33          <h2>Coś poszło nie tak z GraphQL</h2>
34          <details>
35            <summary>Szczegóły błędu</summary>
36            <pre>{this.state.error?.message}</pre>
37          </details>
38          <button onClick={() => this.setState({ hasError: false, error: null })}>
39            Spróbuj ponownie
40          </button>
41        </div>
42      );
43    }
44
45    return this.props.children;
46  }
47}
48
49// Użycie
50function App() {
51  return (
52    <ApolloProvider client={apolloClient}>
53      <GraphQLErrorBoundary>
54        <Router>
55          {/* Twoja aplikacja */}
56        </Router>
57      </GraphQLErrorBoundary>
58    </ApolloProvider>
59  );
60}

4. Testing z Apollo Client

1// __tests__/UsersList.test.js
2import React from 'react';
3import { render, screen, waitFor } from '@testing-library/react';
4import { MockedProvider } from '@apollo/client/testing';
5import UsersList, { GET_USERS } from '../UsersList';
6
7const mocks = [
8  {
9    request: {
10      query: GET_USERS,
11      variables: { limit: 10, offset: 0 }
12    },
13    result: {
14      data: {
15        users: [
16          {
17            id: '1',
18            name: 'John Doe',
19            email: 'john@example.com',
20            avatar: 'https://example.com/avatar1.jpg',
21            posts: []
22          }
23        ]
24      }
25    }
26  }
27];
28
29test('renders users list', async () => {
30  render(
31    <MockedProvider mocks={mocks} addTypename={false}>
32      <UsersList />
33    </MockedProvider>
34  );
35
36  // Sprawdź loading state
37  expect(screen.getByText('Ładowanie użytkowników...')).toBeInTheDocument();
38
39  // Poczekaj na załadowanie danych
40  await waitFor(() => {
41    expect(screen.getByText('John Doe')).toBeInTheDocument();
42  });
43});
44
45test('handles error state', async () => {
46  const errorMocks = [
47    {
48      request: {
49        query: GET_USERS,
50        variables: { limit: 10, offset: 0 }
51      },
52      error: new Error('Network error')
53    }
54  ];
55
56  render(
57    <MockedProvider mocks={errorMocks} addTypename={false}>
58      <UsersList />
59    </MockedProvider>
60  );
61
62  await waitFor(() => {
63    expect(screen.getByText(/Błąd: Network error/)).toBeInTheDocument();
64  });
65});

Kiedy uzywac Apollo Client?

Apollo Client jest najlepszym wyborem gdy:

  • Backend uzywa GraphQL -- Apollo jest zoptymalizowany pod ten protokol
  • Potrzebujesz zaawansowanego cache'owania -- Apollo InMemoryCache automatycznie normalizuje i deduplikuje dane
  • Twoja aplikacja wymaga optymistycznych aktualizacji -- np. "lajkowanie" posta wyswietla zmiane natychmiast, zanim serwer potwierdzi
  • Pracujesz z real-time danymi -- subskrypcje GraphQL pozwalaja na komunikacje w czasie rzeczywistym
  • Chcesz DevTools -- Apollo DevTools w przegladarce pozwalaja podgladac cache, zapytania i mutacje

Alternatywy dla Apollo Client to:

  • urql -- lzejsza biblioteka GraphQL z prostszym API
  • React Query + graphql-request -- React Query do cache'owania + prosty klient GraphQL
  • Relay -- rozwiazanie Mety, zoptymalizowane pod duze aplikacje

Apollo Client to kompleksowe rozwiązanie do zarządzania stanem GraphQL w aplikacjach React. Oferuje potężne funkcje cache'owania, optymistycznych aktualizacji i real-time subscriptions, które pozwalają na tworzenie wydajnych i responsywnych aplikacji -- jak zaawansowany system komunikacji na statku kosmicznym, który nie tylko odbiera sygnaly, ale tez inteligentnie je buforuje i przetwarza.

Vai a CodeWorlds