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.
1npm install @apollo/client graphql
2# Lub z dodatkowym toolingiem
3npm install @apollo/client graphql apollo-link-error apollo-upload-client1// 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;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;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}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}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}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}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});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}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`;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}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}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});Apollo Client jest najlepszym wyborem gdy:
Alternatywy dla Apollo Client to:
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.