Web Development

GraphQL dengan Apollo Client: Panduan Lengkap

Tutorial lengkap belajar GraphQL dengan Apollo Client β€” queries, mutations, cache management, real-time subscriptions, dan optimasi performa dengan contoh kode praktis

1. Pengenalan GraphQL

GraphQL adalah bahasa query untuk API yang dikembangkan oleh Facebook (Meta) pada tahun 2012 dan dirilis sebagai open-source pada tahun 2015. GraphQL memungkinkan client untuk meminta data yang tepat dibutuhkan β€” tidak lebih, tidak kurang β€” dalam satu permintaan tunggal.

Berbeda dengan REST API yang menggunakan banyak endpoints berbeda, GraphQL menggunakan satu endpoint tunggal dengan fleksibilitas query yang tinggi. Ini mengatasi masalah over-fetching (mengambil data berlebihan) dan under-fetching (data kurang) yang sering terjadi di REST.

Konsep Inti GraphQL

Konsep Penjelasan
SchemaDefinisi tipe data dan hubungan antar entitas β€” blueprint dari API Anda
QueryOperasi untuk mengambil data (read) β€” seperti GET di REST
MutationOperasi untuk mengubah data (create, update, delete) β€” seperti POST/PUT/DELETE
SubscriptionOperasi untuk mendengarkan perubahan data secara real-time
Type SystemSistem tipe yang ketat untuk memvalidasi data yang diminta
ResolverFungsi yang bertugas mengambil data untuk setiap field dalam schema

Struktur Schema GraphQL

GraphQL Schema
# Definisi tipe untuk User
type User {
  id: ID!
  nama: String!
  email: String!
  artikel: [Artikel!]!
  createdAt: String
}

# Definisi tipe untuk Artikel
type Artikel {
  id: ID!
  judul: String!
  konten: String!
  penulis: User!
  tag: [String!]!
  dipublikasikan: Boolean!
  views: Int
}

# Query root type
type Query {
  users: [User!]!
  user(id: ID!): User
  artikel(id: ID!): Artikel
  semuaArtikel(limit: Int, offset: Int): [Artikel!]!
}

# Mutation root type
type Mutation {
  createUser(nama: String!, email: String!): User!
  updateArtikel(id: ID!, judul: String, konten: String): Artikel!
  deleteArtikel(id: ID!): Boolean!
}

# Subscription root type
type Subscription {
  artikelBaru: Artikel!
  userOnline: User!
}
Diagram: Arsitektur GraphQL
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   CLIENT (React App)                     β”‚
β”‚                                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚   Query    β”‚  β”‚  Mutation  β”‚  β”‚   Subscription     β”‚ β”‚
β”‚  β”‚  GET data  β”‚  β”‚ MODIFY dataβ”‚  β”‚  REALTIME data     β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚        β”‚               β”‚                   β”‚             β”‚
β”‚        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚
β”‚                        β”‚                                 β”‚
β”‚                  β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”                          β”‚
β”‚                  β”‚ Apollo     β”‚                          β”‚
β”‚                  β”‚ Client     β”‚                          β”‚
β”‚                  β”‚ (Cache)    β”‚                          β”‚
β”‚                  β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚  Single Endpoint
                         β”‚  POST /graphql
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”                           β”‚
β”‚                  β”‚  GraphQL   β”‚                           β”‚
β”‚                  β”‚  Server    β”‚                           β”‚
β”‚                  β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜                           β”‚
β”‚                        β”‚                                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ Database │◄──│ Resolvers  │──►│ External APIs     β”‚  β”‚
β”‚  β”‚ (Postgresβ”‚   β”‚            β”‚   β”‚ (REST, gRPC, dll) β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

2. GraphQL vs REST API

Sebelum melangkah lebih jauh, mari kita pahami perbedaan mendasar antara GraphQL dan REST API agar Anda tahu kapan sebaiknya menggunakan masing-masing.

Aspek REST API GraphQL
EndpointsBanyak (GET /users, POST /posts, dll.)Satu (POST /graphql)
Data FetchingServer menentukan responsClient menentukan data yang diambil
Over-fetchingSering terjadi β€” data berlebihTidak β€” ambil hanya yang dibutuhkan
Under-fetchingPerlu banyak requestSatu request untuk semua data
VersioningPerlu v1, v2, dll.Tidak perlu β€” evolve schema
Type SafetyTergantung dokumentasiBuilt-in type system
Real-timePerlu WebSocket manualSubscription built-in
CachingMudah (HTTP caching)Perlu tool tambahan (Apollo Cache)
Learning Curve🟒 Mudah🟑 Sedang
πŸ’‘ Kapan Menggunakan GraphQL?

GraphQL sangat cocok untuk:

  • Aplikasi dengan banyak relasi data yang kompleks
  • Frontend yang membutuhkan fleksibilitas query tinggi
  • Proyek dengan beberapa client (web, mobile, IoT)
  • Aplikasi real-time (chat, notifikasi, dashboard)

REST masih lebih baik untuk API sederhana, file upload, dan caching HTTP yang agresif.

3. Setup Apollo Client

Apollo Client adalah library state management paling populer untuk GraphQL yang bekerja sangat baik dengan React. Apollo Client menyediakan caching otomatis, pagination, optimistic UI, dan banyak fitur lainnya.

Instalasi

Bash
# Instal Apollo Client dan dependensinya
npm install @apollo/client graphql

# Atau dengan yarn
yarn add @apollo/client graphql

# Atau dengan pnpm
pnpm add @apollo/client graphql

# Output:
# + @apollo/client@3.10.x
# + graphql@16.x.x

Konfigurasi ApolloProvider

JavaScript β€” src/apollo/client.js
import { ApolloClient, InMemoryCache, HttpLink, split } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { getMainDefinition } from '@apollo/client/utilities';
import { createClient } from 'graphql-ws';

// HTTP Link untuk queries dan mutations
const httpLink = new HttpLink({
  uri: 'http://localhost:4000/graphql',
  credentials: 'include', // Kirim cookies
});

// WebSocket Link untuk subscriptions
const wsLink = new GraphQLWsLink(
  createClient({
    url: 'ws://localhost:4000/graphql',
    connectionParams: () => {
      const token = localStorage.getItem('auth-token');
      return { authToken: token };
    },
  })
);

// Split link: kirim operasi ke link yang tepat
const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,    // Jika subscription β†’ WebSocket
  httpLink   // Jika query/mutation β†’ HTTP
);

// Buat Apollo Client
const client = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          semuaArtikel: {
            // Merge untuk pagination
            keyArgs: false,
            merge(existing = [], incoming) {
              return [...existing, ...incoming];
            },
          },
        },
      },
    },
  }),
  defaultOptions: {
    watchQuery: {
      fetchPolicy: 'cache-and-network',
    },
  },
});

export default client;

Wrap Aplikasi dengan ApolloProvider

JSX β€” src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ApolloProvider } from '@apollo/client';
import client from './apollo/client';
import App from './App';
import './index.css';

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

// Sekarang semua komponen di dalam App bisa
// menggunakan hooks Apollo (useQuery, useMutation, dll.)
⚠️ Perhatian

Pastikan GraphQL server sudah berjalan sebelum menjalankan client. Untuk development, Anda bisa menggunakan Apollo Server atau GraphQL Yoga sebagai server lokal.

4. Queries: Mengambil Data

Query adalah operasi GraphQL untuk mengambil data. Dengan Apollo Client, Anda menggunakan useQuery hook untuk mengirim query dan mendapatkan hasilnya secara reaktif.

Definisi Query dengan gql

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

// Query untuk mengambil semua artikel
export const GET_ALL_ARTIKEL = gql`
  query GetAllArtikel($limit: Int, $offset: Int) {
    semuaArtikel(limit: $limit, offset: $offset) {
      id
      judul
      konten
      tag
      dipublikasikan
      views
      penulis {
        id
        nama
        email
      }
    }
  }
`;

// Query untuk mengambil satu artikel berdasarkan ID
export const GET_ARTIKEL = gql`
  query GetArtikel($id: ID!) {
    artikel(id: $id) {
      id
      judul
      konten
      tag
      dipublikasikan
      views
      createdAt
      penulis {
        id
        nama
        email
      }
    }
  }
`;

// Query untuk mengambil semua user
export const GET_USERS = gql`
  query GetUsers {
    users {
      id
      nama
      email
      createdAt
      artikel {
        id
        judul
      }
    }
  }
`;

// Query untuk mengambil satu user
export const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      nama
      email
      artikel {
        id
        judul
        views
      }
    }
  }
`;

Menggunakan useQuery Hook

JSX β€” src/components/DaftarArtikel.jsx
import { useQuery } from '@apollo/client';
import { GET_ALL_ARTIKEL } from '../graphql/queries';

function DaftarArtikel() {
  const { loading, error, data, refetch, fetchMore } = useQuery(
    GET_ALL_ARTIKEL,
    {
      variables: { limit: 10, offset: 0 },
      // fetchPolicy options:
      // 'cache-first'   β†’ cek cache dulu (default)
      // 'network-only'  β†’ selalu dari network
      // 'cache-only'    β†’ hanya dari cache
      // 'no-cache'      β†’ tidak menyimpan ke cache
      // 'cache-and-network' β†’ keduanya
      fetchPolicy: 'cache-and-network',
      // Polling untuk auto-refresh setiap 30 detik
      // pollInterval: 30000,
    }
  );

  if (loading) return <div className="skeleton">Memuat artikel...</div>;
  if (error) return <p className="error">Error: {error.message}</p>;

  const { semuaArtikel } = data;

  const handleLoadMore = () => {
    fetchMore({
      variables: { offset: semuaArtikel.length },
      updateQuery: (prev, { fetchMoreResult }) => {
        if (!fetchMoreResult.semuArtikel.length) return prev;
        return {
          semuaArtikel: [
            ...prev.semuArtikel,
            ...fetchMoreResult.semuArtikel,
          ],
        };
      },
    });
  };

  return (
    <div className="daftar-artikel">
      <h2>Semua Artikel ({semuaArtikel.length})</h2>

      <button onClick={() => refetch()} className="btn-refresh">
        πŸ”„ Refresh Data
      </button>

      <div className="artikel-grid">
        {semuaArtikel.map((artikel) => (
          <div key={artikel.id} className="artikel-card">
            <h3>{artikel.judul}</h3>
            <p>Penulis: {artikel.penulis.nama}</p>
            <p>Views: {artikel.views}</p>
            <div className="tags">
              {artikel.tag.map((t) => (
                <span key={t} className="tag">{t}</span>
              ))}
            </div>
          </div>
        ))}
      </div>

      <button onClick={handleLoadMore} className="btn-loadmore">
        Muat Lebih Banyak...
      </button>
    </div>
  );
}

export default DaftarArtikel;

Lazy Query: Query yang Dipicu Manual

JSX β€” src/components/CariArtikel.jsx
import { useLazyQuery } from '@apollo/client';
import { GET_ARTIKEL } from '../graphql/queries';
import { useState } from 'react';

function CariArtikel() {
  const [idArtikel, setIdArtikel] = useState('');

  // useLazyQuery: query tidak dieksekusi sampai fungsi dipanggil
  const [getArtikel, { loading, error, data }] = useLazyQuery(
    GET_ARTIKEL,
    {
      // Simpan hasil ke cache
      fetchPolicy: 'cache-and-network',
    }
  );

  const handleCari = () => {
    if (idArtikel.trim()) {
      getArtikel({ variables: { id: idArtikel.trim() } });
    }
  };

  return (
    <div className="cari-artikel">
      <h2>Cari Artikel by ID</h2>

      <div className="search-form">
        <input
          type="text"
          value={idArtikel}
          onChange={(e) => setIdArtikel(e.target.value)}
          placeholder="Masukkan ID artikel..."
        />
        <button onClick={handleCari} disabled={loading}>
          {loading ? 'Mencari...' : 'πŸ” Cari'}
        </button>
      </div>

      {error && <p className="error">{error.message}</p>}

      {data?.artikel && (
        <div className="artikel-detail">
          <h3>{data.artikel.judul}</h3>
          <p>{data.artikel.konten}</p>
          <p>Penulis: {data.artikel.penulis.nama}</p>
          <p>Views: {data.artikel.views}</p>
        </div>
      )}
    </div>
  );
}

export default CariArtikel;

5. Mutations: Mengubah Data

Mutation digunakan untuk membuat, memperbarui, atau menghapus data. Dengan Apollo Client, Anda menggunakan useMutation hook untuk melakukan operasi mutasi.

Definisi Mutations

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

// Membuat user baru
export const CREATE_USER = gql`
  mutation CreateUser($nama: String!, $email: String!) {
    createUser(nama: $nama, email: $email) {
      id
      nama
      email
      createdAt
    }
  }
`;

// Memperbarui artikel
export const UPDATE_ARTIKEL = gql`
  mutation UpdateArtikel(
    $id: ID!
    $judul: String
    $konten: String
    $tag: [String!]
    $dipublikasikan: Boolean
  ) {
    updateArtikel(
      id: $id
      judul: $judul
      konten: $konten
      tag: $tag
      dipublikasikan: $dipublikasikan
    ) {
      id
      judul
      konten
      tag
      dipublikasikan
    }
  }
`;

// Menghapus artikel
export const DELETE_ARTIKEL = gql`
  mutation DeleteArtikel($id: ID!) {
    deleteArtikel(id: $id)
  }
`;

// Membuat artikel baru
export const CREATE_ARTIKEL = gql`
  mutation CreateArtikel(
    $judul: String!
    $konten: String!
    $tag: [String!]!
    $dipublikasikan: Boolean
  ) {
    createArtikel(
      judul: $judul
      konten: $konten
      tag: $tag
      dipublikasikan: $dipublikasikan
    ) {
      id
      judul
      konten
      penulis {
        id
        nama
      }
    }
  }
`;

Menggunakan useMutation Hook

JSX β€” src/components/BuatArtikel.jsx
import { useState } from 'react';
import { useMutation } from '@apollo/client';
import { CREATE_ARTIKEL } from '../graphql/mutations';
import { GET_ALL_ARTIKEL } from '../graphql/queries';

function BuatArtikel() {
  const [formData, setFormData] = useState({
    judul: '',
    konten: '',
    tag: '',
    dipublikasikan: false,
  });

  // useMutation mengembalikan [mutateFunction, { data, loading, error }]
  const [createArtikel, { loading, error }] = useMutation(
    CREATE_ARTIKEL,
    {
      // Refetch queries setelah mutation berhasil
      refetchQueries: [
        { query: GET_ALL_ARTIKEL, variables: { limit: 10, offset: 0 } },
      ],

      // Atau gunakan update untuk manipulasi cache manual
      // update(cache, { data: { createArtikel } }) {
      //   const existing = cache.readQuery({
      //     query: GET_ALL_ARTIKEL,
      //     variables: { limit: 10, offset: 0 },
      //   });
      //   cache.writeQuery({
      //     query: GET_ALL_ARTIKEL,
      //     variables: { limit: 10, offset: 0 },
      //     data: {
      //       semuaArtikel: [createArtikel, ...existing.semuArtikel],
      //     },
      //   });
      // },

      // Optimistic UI: tampilkan hasil sebelum server merespons
      optimisticResponse: {
        createArtikel: {
          __typename: 'Artikel',
          id: 'temp-' + Date.now(),
          judul: formData.judul,
          konten: formData.konten,
          penulis: { __typename: 'User', id: 'current', nama: 'Anda' },
        },
      },

      onCompleted: (data) => {
        console.log('Artikel dibuat:', data.createArtikel);
        setFormData({ judul: '', konten: '', tag: '', dipublikasikan: false });
      },

      onError: (err) => {
        console.error('Gagal membuat artikel:', err.message);
      },
    }
  );

  const handleSubmit = (e) => {
    e.preventDefault();
    createArtikel({
      variables: {
        judul: formData.judul,
        konten: formData.konten,
        tag: formData.tag.split(',').map((t) => t.trim()),
        dipublikasikan: formData.dipublikasikan,
      },
    });
  };

  return (
    <form onSubmit={handleSubmit} className="form-artikel">
      <h2>Buat Artikel Baru</h2>

      <div className="form-group">
        <label>Judul</label>
        <input
          type="text"
          value={formData.judul}
          onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
          required
        />
      </div>

      <div className="form-group">
        <label>Konten</label>
        <textarea
          value={formData.konten}
          onChange={(e) => setFormData({ ...formData, konten: e.target.value })}
          rows={6}
          required
        />
      </div>

      <div className="form-group">
        <label>Tag (pisahkan koma)</label>
        <input
          type="text"
          value={formData.tag}
          onChange={(e) => setFormData({ ...formData, tag: e.target.value })}
          placeholder="react, graphql, tutorial"
        />
      </div>

      <div className="form-group checkbox">
        <label>
          <input
            type="checkbox"
            checked={formData.dipublikasikan}
            onChange={(e) =>
              setFormData({ ...formData, dipublikasikan: e.target.checked })
            }
          />
          Langsung publikasikan
        </label>
      </div>

      {error && <p className="error">Error: {error.message}</p>}

      <button type="submit" disabled={loading}>
        {loading ? 'Menyimpan...' : 'πŸ“ Buat Artikel'}
      </button>
    </form>
  );
}

export default BuatArtikel;
πŸ’‘ Optimistic UI

Optimistic UI memungkinkan UI diperbarui sebelum server merespons. Ini membuat aplikasi terasa lebih responsif. Jika server mengembalikan error, Apollo akan otomatis rollback perubahan ke state sebelumnya.

6. Cache Management

Salah satu fitur terbaik Apollo Client adalah caching otomatis. Setiap query yang dikirimkan, hasilnya otomatis disimpan di cache lokal. Ini mengurangi request ke server dan membuat UI sangat responsif.

Cara Kerja Cache

Diagram: Apollo Cache Flow
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Apollo Cache (InMemoryCache)        β”‚
β”‚                                                  β”‚
β”‚  Cache Normalized:                               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚ "Artikel:1" β†’ { id:1, judul:"React", ... } β”‚ β”‚
β”‚  β”‚ "Artikel:2" β†’ { id:2, judul:"Node", ... }  β”‚ β”‚
β”‚  β”‚ "User:1"    β†’ { id:1, nama:"Budi", ... }   β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚                                                  β”‚
β”‚  When Query is sent:                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  Query   │───►│  Cache  │───►│  Network  β”‚  β”‚
β”‚  β”‚  sent    β”‚    β”‚  check  β”‚    β”‚  (jika    β”‚  β”‚
β”‚  β”‚          β”‚    β”‚         β”‚    β”‚  perlu)   β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                                  β”‚
β”‚  Fetch Policies:                                 β”‚
β”‚  β€’ cache-first: cek cache dulu, network jika missβ”‚
β”‚  β€’ cache-only: hanya dari cache                  β”‚
β”‚  β€’ network-only: hanya dari network              β”‚
β”‚  β€’ no-cache: tanpa cache sama sekali             β”‚
β”‚  β€’ cache-and-network: keduanya (update bg)       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Cache Type Policies

JavaScript β€” Cache Configuration
import { InMemoryCache, makeVar } from '@apollo/client';

// Reactive variables β€” global state di luar cache
export const isLoggedInVar = makeVar(false);
export const currentUserVar = makeVar(null);

const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        // Pagination: merge results
        semuaArtikel: {
          keyArgs: ['filter', 'tag'], // Cache terpisah per filter/tag
          merge(existing = [], incoming, { args }) {
            if (args?.offset === 0) return incoming; // Reset
            return [...existing, ...incoming];
          },
          read(existing, { args }) {
            if (args?.offset && !existing) return undefined;
            return existing;
          },
        },

        // Custom field: gabungkan data dari cache
        artikelPopuler: {
          read(existing) {
            return existing || [];
          },
        },

        // Reactive variable field
        isLoggedIn: {
          read() {
            return isLoggedInVar();
          },
        },

        currentUser: {
          read() {
            return currentUserVar();
          },
        },
      },
    },

    // Merge strategy untuk Artikel type
    Artikel: {
      keyFields: ['id'], // Default, bisa custom: ['id', 'versi']
      fields: {
        views: {
          // Optimistic update untuk views count
          merge(existing = 0, incoming) {
            return Math.max(existing, incoming);
          },
        },
      },
    },

    // Merge strategy untuk User type
    User: {
      keyFields: ['id'],
      fields: {
        artikel: {
          merge(existing = [], incoming) {
            return incoming; // Replace, bukan merge
          },
        },
      },
    },
  },
});

export default cache;

Manual Cache Manipulation

JavaScript β€” Cache Operations
import { useApolloClient } from '@apollo/client';

function CacheController() {
  const client = useApolloClient();

  // Baca data dari cache
  const bacaCache = () => {
    const cached = client.readQuery({
      query: GET_ALL_ARTIKEL,
      variables: { limit: 10, offset: 0 },
    });
    console.log('Cache contents:', cached);
  };

  // Tulis data ke cache
  const tulisCache = () => {
    client.writeQuery({
      query: GET_ALL_ARTIKEL,
      variables: { limit: 10, offset: 0 },
      data: {
        semuaArtikel: [
          {
            __typename: 'Artikel',
            id: '99',
            judul: 'Ditulis langsung ke cache',
            konten: 'Ini data manipulasi cache',
            tag: ['cache'],
            dipublikasikan: true,
            views: 0,
            penulis: { __typename: 'User', id: '1', nama: 'Admin' },
          },
        ],
      },
    });
  };

  // Baca fragment dari cache
  const bacaFragment = () => {
    const fragment = client.readFragment({
      id: 'Artikel:1', // Cache ID: TypeName:KeyValue
      fragment: gql`
        fragment DetailArtikel on Artikel {
          id
          judul
          views
        }
      `,
    });
    console.log('Fragment data:', fragment);
  };

  // Reset seluruh cache
  const resetCache = async () => {
    await client.resetStore();
    console.log('Cache di-reset!');
  };

  // Hapus cache tertentu
  const hapusCacheItem = (artikelId) => {
    client.cache.evict({ id: `Artikel:${artikelId}` });
    client.cache.gc(); // Garbage collection
  };

  return (
    <div className="cache-controls">
      <button onClick={bacaCache}>πŸ“– Baca Cache</button>
      <button onClick={tulisCache}>✏️ Tulis Cache</button>
      <button onClick={bacaFragment}>🧩 Baca Fragment</button>
      <button onClick={resetCache}>πŸ”„ Reset Cache</button>
    </div>
  );
}

7. Subscriptions: Real-time Data

Subscription memungkinkan Anda mendengarkan perubahan data secara real-time menggunakan WebSocket. Ini sangat berguna untuk fitur seperti chat, notifikasi live, dan dashboard monitoring.

Definisi Subscriptions

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

// Subscription: artikel baru
export const ON_ARTIKEL_BARU = gql`
  subscription OnArtikelBaru {
    artikelBaru {
      id
      judul
      konten
      tag
      dipublikasikan
      penulis {
        id
        nama
      }
    }
  }
`;

// Subscription: user online
export const ON_USER_ONLINE = gql`
  subscription OnUserOnline {
    userOnline {
      id
      nama
      email
    }
  }
`;

// Subscription: perubahan pada artikel tertentu
export const ON_ARTIKEL_UPDATED = gql`
  subscription OnArtikelUpdated($artikelId: ID!) {
    artikelUpdated(id: $artikelId) {
      id
      judul
      konten
      views
      dipublikasikan
    }
  }
`;

Menggunakan useSubscription Hook

JSX β€” src/components/NotifikasiRealtime.jsx
import { useEffect, useState } from 'react';
import { useSubscription, useApolloClient } from '@apollo/client';
import { ON_ARTIKEL_BARU, ON_USER_ONLINE } from '../graphql/subscriptions';
import { GET_ALL_ARTIKEL } from '../graphql/queries';

function NotifikasiRealtime() {
  const [notifikasi, setNotifikasi] = useState([]);
  const client = useApolloClient();

  // Subscription untuk artikel baru
  const { data: artikelData, loading: artikelLoading } = useSubscription(
    ON_ARTIKEL_BARU,
    {
      onSubscriptionData: ({ subscriptionData }) => {
        const artikel = subscriptionData.data.artikelBaru;
        console.log('Artikel baru diterima:', artikel);

        // Tambahkan notifikasi
        setNotifikasi((prev) => [
          { id: Date.now(), type: 'artikel', message: `Artikel baru: "${artikel.judul}" oleh ${artikel.penulis.nama}` },
          ...prev,
        ]);

        // Update cache: tambahkan artikel baru ke daftar
        try {
          const cached = client.readQuery({
            query: GET_ALL_ARTIKEL,
            variables: { limit: 10, offset: 0 },
          });

          if (cached) {
            client.writeQuery({
              query: GET_ALL_ARTIKEL,
              variables: { limit: 10, offset: 0 },
              data: {
                semuaArtikel: [artikel, ...cached.semuArtikel],
              },
            });
          }
        } catch (e) {
          // Cache belum diinisialisasi
          console.log('Cache belum siap, skip update');
        }
      },
    }
  );

  // Subscription untuk user online
  const { data: userData } = useSubscription(ON_USER_ONLINE, {
    onSubscriptionData: ({ subscriptionData }) => {
      const user = subscriptionData.data.userOnline;
      setNotifikasi((prev) => [
        { id: Date.now(), type: 'user', message: `${user.nama} sedang online` },
        ...prev.slice(0, 19), // Simpan max 20 notifikasi
      ]);
    },
  });

  const hapusNotifikasi = (id) => {
    setNotifikasi((prev) => prev.filter((n) => n.id !== id));
  };

  return (
    <div className="notifikasi-panel">
      <h3>πŸ”” Notifikasi Real-time</h3>

      {artikelLoading && <p>Menunggu subscription...</p>}

      {notifikasi.length === 0 ? (
        <p className="no-data">Belum ada notifikasi</p>
      ) : (
        <ul className="notifikasi-list">
          {notifikasi.map((n) => (
            <li key={n.id} className={`notif-item notif-${n.type}`}>
              <span>{n.type === 'artikel' ? 'πŸ“' : 'πŸ‘€'}</span>
              <span>{n.message}</span>
              <button onClick={() => hapusNotifikasi(n.id)}>βœ•</button>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default NotifikasiRealtime;
πŸ“‹ Server-side Subscriptions

Untuk subscriptions berfungsi, server GraphQL perlu mendukung WebSocket. Apollo Server menggunakan graphql-ws package. Pastikan server mengimplementasikan PubSub untuk publish data real-time ke subscriber.

8. Error Handling

Menangani error dengan baik sangat penting dalam aplikasi GraphQL. Apollo Client menyediakan beberapa cara untuk menangani error β€” dari tingkat komponen hingga global.

Error Handling per Komponen

JSX β€” Error Handling
import { useQuery, useMutation, ApolloError } from '@apollo/client';
import { GET_ALL_ARTIKEL } from '../graphql/queries';

function KomponenDenganErrorHandling() {
  const { loading, error, data } = useQuery(GET_ALL_ARTIKEL, {
    // Error policy: 'none' (default), 'all' (termasuk partial data)
    errorPolicy: 'all',
    // Retry on network error
    onError: (error) => {
      console.error('Query error:', error);
      if (error.networkError) {
        // Tampilkan toast: "Koneksi bermasalah"
        showToast('Koneksi bermasalah, coba lagi nanti');
      }
      if (error.graphQLErrors) {
        error.graphQLErrors.forEach((err) => {
          console.log('GraphQL Error:', err.message, err.extensions);
          if (err.extensions?.code === 'UNAUTHENTICATED') {
            // Redirect ke login
            window.location.href = '/login';
          }
        });
      }
    },
  });

  if (loading) return <LoadingSpinner />;

  // error.networkError: masalah jaringan
  // error.graphQLErrors: error dari GraphQL server
  // error.message: pesan error gabungan

  if (error) {
    return (
      <div className="error-container">
        <h3>❌ Terjadi Kesalahan</h3>

        {error.networkError && (
          <p className="network-error">
            Koneksi ke server gagal. Periksa koneksi internet Anda.
          </p>
        )}

        {error.graphQLErrors.map(({ message, extensions }, idx) => (
          <div key={idx} className="graphql-error">
            <p>{message}</p>
            {extensions?.code && (
              <span className="error-code">Code: {extensions.code}</span>
            )}
          </div>
        ))}

        <button onClick={() => window.location.reload()}>
          πŸ”„ Coba Lagi
        </button>
      </div>
    );
  }

  return <DaftarArtikel data={data} />;
}

Global Error Link

JavaScript β€” src/apollo/errorLink.js
import { onError } from '@apollo/client/link/error';
import { fromPromise } from '@apollo/client';
import { refreshToken } from './auth';

const errorLink = onError(
  ({ graphQLErrors, networkError, operation, forward }) => {
    // Handle GraphQL errors
    if (graphQLErrors) {
      for (const err of graphQLErrors) {
        switch (err.extensions?.code) {
          case 'UNAUTHENTICATED':
            // Token expired, coba refresh
            return fromPromise(
              refreshToken().catch(() => {
                // Refresh gagal β†’ logout
                localStorage.removeItem('auth-token');
                window.location.href = '/login';
                return null;
              })
            ).flatMap(() => forward(operation));

          case 'FORBIDDEN':
            console.error('Akses ditolak:', err.message);
            break;

          case 'BAD_USER_INPUT':
            console.warn('Input tidak valid:', err.message);
            break;

          default:
            console.error('GraphQL Error:', err.message);
        }
      }
    }

    // Handle network errors
    if (networkError) {
      console.error('Network Error:', networkError);

      // Retry logic untuk 5xx errors
      if (networkError.statusCode >= 500) {
        console.log('Server error, akan dicoba lagi...');
      }
    }
  }
);

export default errorLink;

9. Optimasi & Best Practices

Untuk membangun aplikasi GraphQL yang performan dan mudah di-maintain, berikut adalah best practices yang harus diterapkan:

Fragment Reuse

JavaScript β€” Fragments
import { gql } from '@apollo/client';

// Definisikan fragment untuk reuse
export const ARTIKEL_CORE_FIELDS = gql`
  fragment ArtikelCoreFields on Artikel {
    id
    judul
    konten
    tag
    dipublikasikan
    views
  }
`;

export const PENULIS_FIELDS = gql`
  fragment PenulisFields on User {
    id
    nama
    email
  }
`;

// Gunakan fragment di queries
export const GET_ALL_ARTIKEL = gql`
  ${ARTIKEL_CORE_FIELDS}
  ${PENULIS_FIELDS}
  query GetAllArtikel($limit: Int, $offset: Int) {
    semuaArtikel(limit: $limit, offset: $offset) {
      ...ArtikelCoreFields
      penulis {
        ...PenulisFields
      }
    }
  }
`;

// Fragment juga bisa nested
export const ARTIKEL_DETAIL = gql`
  ${ARTIKEL_CORE_FIELDS}
  ${PENULIS_FIELDS}
  query GetArtikelDetail($id: ID!) {
    artikel(id: $id) {
      ...ArtikelCoreFields
      createdAt
      penulis {
        ...PenulisFields
        artikel {
          id
          judul
          views
        }
      }
    }
  }
`;

Code Generation dengan GraphQL Codegen

Bash & TypeScript
# Instal GraphQL Codegen
npm install -D @graphql-codegen/cli @graphql-codegen/typescript \
  @graphql-codegen/typescript-operations \
  @graphql-codegen/typescript-react-apollo

# Buat konfigurasi
cat > codegen.yml << 'EOF'
schema: "http://localhost:4000/graphql"
documents: "src/**/*.{ts,tsx,graphql}"
generates:
  src/generated/graphql.tsx:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo
    config:
      withHooks: true
      withComponent: false
      withHOC: false
EOF

# Jalankan codegen
npx graphql-codegen

# Output: src/generated/graphql.tsx
# β†’ Types dan hooks auto-generated dari schema!
# β†’ useGetAllArtikelQuery(), useCreateArtikelMutation(), dll.
πŸ’‘ Best Practices
  • Gunakan fragments untuk menghindari duplikasi field definitions
  • Pilih fetch policy yang tepat β€” cache-first untuk data yang jarang berubah, network-only untuk data yang sering update
  • Paginate dengan benar β€” gunakan cursor-based pagination untuk dataset besar
  • Optimistic UI untuk operasi create/update agar UI terasa instan
  • Codegen untuk type safety dan auto-generated hooks
  • Persist queries di production untuk keamanan dan performa
  • Monitor cache size β€” gunakan client.cache.gc() secara berkala

Tips Performa

Teknik Penjelasan Dampak
Persisted QueriesKirim hash query, bukan query string🟒 Kurangi bandwidth 70%+
Query BatchingGabungkan beberapa query dalam satu request🟒 Kurangi HTTP requests
Fragment ColocationLetakkan fragment dekat komponen yang menggunakannya🟑 Mudah di-maintain
Lazy QueriesHanya fetch saat dibutuhkan (on-demand)🟒 Hemat bandwidth
Cache NormalizationData dinormalisasi otomatis oleh InMemoryCache🟒 Konsistensi data
Field PoliciesKustomisasi merge/read per field🟑 Fine-grained control

10. Quiz Pemahaman

Uji pemahaman Anda tentang GraphQL dan Apollo Client dengan quiz berikut:

1. Apa keunggulan utama GraphQL dibanding REST API?

2. Hook apa yang digunakan untuk mengambil data di Apollo Client?

3. Apa fungsi dari InMemoryCache di Apollo Client?

4. Operasi GraphQL apa yang digunakan untuk data real-time?

5. Apa yang dimaksud dengan "Optimistic UI" di Apollo Client?

πŸ” Zoom
100%
🎨 Tema