GraphQL w React: Rewolucja w pobieraniu danych

2/15/2025 Frontend

Mateusz Kędziora

image

Budowanie nowoczesnych aplikacji webowych z użyciem Reacta to standard. Ale sposób pobierania danych, tradycyjnie przez REST API, bywa problematyczny. Tutaj wkracza GraphQL – technologia, która zmienia zasady gry. W tym artykule pokażemy, dlaczego GraphQL zyskuje na popularności w świecie Reacta, jak działa i jakie korzyści przynosi w porównaniu do REST. Zapraszam do lektury!

Wprowadzenie do GraphQL

GraphQL to język zapytań (query language) do API i jednocześnie środowisko uruchomieniowe po stronie serwera, które realizuje te zapytania z wykorzystaniem istniejących danych. Brzmi skomplikowanie? Spokojnie. Najważniejsze jest zrozumienie, że GraphQL daje klientowi (np. aplikacji React) pełną kontrolę nad tym, jakie dane chce otrzymać z serwera. Koniec z pobieraniem “na zapas” i marnowaniem zasobów!

Zanim przejdziemy dalej, porównajmy to z typowym podejściem REST.

REST – Status Quo

W architekturze REST komunikacja opiera się na zasobach (np. użytkownik, produkt) identyfikowanych przez adresy URL (endpointy). Aplikacja kliencka wysyła żądanie HTTP do konkretnego endpointu, a serwer zwraca reprezentację tego zasobu (najczęściej w formacie JSON).

Problem z REST? Często endpoint zwraca więcej danych, niż aplikacja potrzebuje (tzw. over-fetching), albo żeby zdobyć wszystkie potrzebne informacje, trzeba wykonać kilka oddzielnych żądań (tzw. under-fetching).

Przykład REST

Załóżmy, że mamy aplikację wyświetlającą listę książek i chcemy pokazać tytuł, autora i liczbę stron każdej książki. W REST API moglibyśmy mieć endpoint /books, który zwraca:

[
  {
    "id": 1,
    "title": "Władca Pierścieni",
    "author": "J.R.R. Tolkien",
    "pages": 1178,
    "genre": "Fantasy",
    "publication_date": "1954-07-29"
  },
  {
    "id": 2,
    "title": "Hobbit",
    "author": "J.R.R. Tolkien",
    "pages": 310,
    "genre": "Fantasy",
    "publication_date": "1937-09-21"
  }
]

Jeżeli potrzebujemy tylko tytułu, autora i liczby stron, pobieramy dużo zbędnych informacji (genre, publication_date). To przykład over-fetchingu.

Przykład GraphQL

W GraphQL zapytanie wyglądałoby tak:

query {
  books {
    title
    author
    pages
  }
}

A serwer zwróci tylko to, o co poprosiliśmy:

{
  "data": {
    "books": [
      {
        "title": "Władca Pierścieni",
        "author": "J.R.R. Tolkien",
        "pages": 1178
      },
      {
        "title": "Hobbit",
        "author": "J.R.R. Tolkien",
        "pages": 310
      }
    ]
  }
}

Dokładnie to, czego potrzebujemy! To esencja GraphQL.

Jak Działa GraphQL?

GraphQL opiera się na kilku kluczowych koncepcjach:

  • Schema (Schemat): Definicja typów danych i relacji między nimi. To “kontrakt” między klientem a serwerem. Schemat określa, jakie zapytania są możliwe i jakie dane można pobrać.
  • Types (Typy): Reprezentują strukturę danych, np. Book, Author. Typy mogą zawierać pola (fields) o określonych typach danych (np. String, Int, Boolean).
  • Query (Zapytanie): Prośba o dane. Klient wysyła zapytanie GraphQL do serwera, określając dokładnie, jakich danych potrzebuje.
  • Mutation (Mutacja): Służy do modyfikacji danych (np. dodawanie, aktualizacja, usuwanie).
  • Resolver (Rozwiązywacz): Funkcja po stronie serwera, która pobiera dane dla konkretnego pola w zapytaniu. Resolver “rozwiązuje” żądanie danych.

Przykład Schematu GraphQL

Oto przykład prostego schematu GraphQL dla biblioteki książek:

type Book {
  id: ID!
  title: String!
  author: Author!
  pages: Int
}

type Author {
  id: ID!
  name: String!
  books: [Book]
}

type Query {
  book(id: ID!): Book
  books: [Book]
  author(id: ID!): Author
  authors: [Author]
}

Wyjaśnienie:

  • type Book: Definiuje typ Book z polami id, title, author i pages. Wykrzyknik (!) oznacza, że pole jest wymagane (nie może być null).
  • type Author: Definiuje typ Author z polami id, name i books. Pole books jest listą typu Book.
  • type Query: Definiuje punkty wejścia do zapytań. Możemy pobrać pojedynczą książkę po jej id (book(id: ID!): Book), listę wszystkich książek (books: [Book]), autora po jego id (author(id: ID!): Author) lub listę wszystkich autorów (authors: [Author]).

Przykład Resolvera

Resolver dla zapytania books mógłby wyglądać tak (przy użyciu Node.js i Express):

const books = [
  { id: '1', title: 'Władca Pierścieni', authorId: '1', pages: 1178 },
  { id: '2', title: 'Hobbit', authorId: '1', pages: 310 },
  { id: '3', title: 'Harry Potter', authorId: '2', pages: 600 }
];

const authors = [
  { id: '1', name: 'J.R.R. Tolkien' },
  { id: '2', name: 'J.K. Rowling' }
];

const resolvers = {
  Query: {
    books: () => books,
    book: (parent, args) => books.find(book => book.id === args.id),
    authors: () => authors,
    author: (parent, args) => authors.find(author => author.id === args.id)
  },
  Book: {
    author: (parent, args) => authors.find(author => author.id === parent.authorId)
  },
  Author: {
    books: (parent, args) => books.filter(book => book.authorId === parent.id)
  }
};

module.exports = resolvers;

Wyjaśnienie:

  • Query: Zawiera resolvry dla zapytań zdefiniowanych w schemacie.
  • books: () => books: Resolver dla zapytania books zwraca po prostu całą tablicę books.
  • book: (parent, args) => books.find(book => book.id === args.id): Resolver dla zapytania book pobiera argument id z zapytania i szuka książki o tym id w tablicy books.
  • Book: { author: ... }: Resolver dla pola author w typie Book. Pobiera authorId z obiektu Book i szuka autora o tym id w tablicy authors.
  • Author: { books: ... }: Resolver dla pola books w typie Author. Pobiera id autora i filtruje listę książek, aby zwrócić tylko te, których authorId odpowiada id autora.

GraphQL w React: Jak to Działa?

Ok, mamy podstawy GraphQL. Teraz zobaczmy, jak to działa w praktyce z Reactem. Potrzebujemy biblioteki, która ułatwi nam komunikację z serwerem GraphQL. Najpopularniejsze opcje to:

  • Apollo Client: Bardzo rozbudowana biblioteka z wieloma funkcjonalnościami, takimi jak zarządzanie stanem, cache, optymistyczne aktualizacje.
  • Relay: Framework od Facebooka, zaprojektowany specjalnie do pracy z GraphQL. Bardziej skomplikowany niż Apollo, ale oferuje zaawansowane funkcje optymalizacji.
  • urql: Lekka i elastyczna biblioteka, łatwa do integracji z Reactem.

W tym artykule skupimy się na Apollo Client, ze względu na jego popularność i bogate możliwości.

Instalacja Apollo Client

npm install @apollo/client graphql

Konfiguracja Apollo Client

import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';

const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql', // Adres Twojego serwera GraphQL
  cache: new InMemoryCache()
});

function App() {
  return (
    <ApolloProvider client={client}>
      {/* Twoja aplikacja React */}
    </ApolloProvider>
  );
}

export default App;

Wyjaśnienie:

  • ApolloClient: Tworzy instancję klienta Apollo.
  • uri: Adres URL serwera GraphQL.
  • cache: Konfiguruje cache w pamięci, co przyspiesza pobieranie danych (nie trzeba za każdym razem wysyłać zapytania do serwera).
  • ApolloProvider: Komponent Reacta, który udostępnia instancję ApolloClient wszystkim komponentom potomnym.

Wykonywanie Zapytań GraphQL w React

Do wykonywania zapytań GraphQL w komponentach React używamy hooka useQuery.

import { useQuery, gql } from '@apollo/client';

const GET_BOOKS = gql`
  query GetBooks {
    books {
      id
      title
      author {
        name
      }
      pages
    }
  }
`;

function BookList() {
  const { loading, error, data } = useQuery(GET_BOOKS);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error : {error.message}</p>;

  return (
    <ul>
      {data.books.map(book => (
        <li key={book.id}>
          {book.title} by {book.author.name} ({book.pages} pages)
        </li>
      ))}
    </ul>
  );
}

export default BookList;

Wyjaśnienie:

  • gql: Funkcja z biblioteki @apollo/client, która parsuje ciąg znaków zawierający zapytanie GraphQL i tworzy obiekt typu DocumentNode.
  • GET_BOOKS: Zapytanie GraphQL pobierające listę książek z serwera. Określamy, jakie pola chcemy pobrać dla każdej książki (id, tytuł, autor (tylko imię), liczba stron).
  • useQuery(GET_BOOKS): Hook Apollo Clienta, który wykonuje zapytanie GET_BOOKS. Zwraca obiekt z właściwościami loading, error i data.
  • loading: true, jeśli zapytanie jest w trakcie wykonywania.
  • error: Obiekt błędu, jeśli wystąpił błąd podczas wykonywania zapytania.
  • data: Dane zwrócone przez serwer GraphQL.
  • Renderowanie: Jeśli dane zostały pobrane (brak loading i error), renderujemy listę książek, pobierając informacje z obiektu data.

Wykonywanie Mutacji GraphQL w React

Do wykonywania mutacji (zmiany danych) używamy hooka useMutation.

import { useMutation, gql } from '@apollo/client';

const ADD_BOOK = gql`
  mutation AddBook($title: String!, $authorId: String!, $pages: Int) {
    addBook(title: $title, authorId: $authorId, pages: $pages) {
      id
      title
      author {
        name
      }
      pages
    }
  }
`;

function AddBookForm() {
  const [addBook, { loading, error }] = useMutation(ADD_BOOK);

  const handleSubmit = (event) => {
    event.preventDefault();
    const title = event.target.title.value;
    const authorId = event.target.authorId.value;
    const pages = parseInt(event.target.pages.value);

    addBook({ variables: { title, authorId, pages } });
  };

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error : {error.message}</p>;

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Title:
        <input type="text" name="title" />
      </label>
      <label>
        Author ID:
        <input type="text" name="authorId" />
      </label>
      <label>
        Pages:
        <input type="number" name="pages" />
      </label>
      <button type="submit">Add Book</button>
    </form>
  );
}

export default AddBookForm;

Wyjaśnienie:

  • ADD_BOOK: Mutacja GraphQL dodająca nową książkę. Określamy, jakie pola chcemy otrzymać w odpowiedzi po dodaniu książki (id, tytuł, autor (tylko imię), liczba stron). Definiujemy również argumenty mutacji ($title, $authorId, $pages).
  • useMutation(ADD_BOOK): Hook Apollo Clienta, który przygotowuje mutację ADD_BOOK. Zwraca funkcję addBook (do wywołania mutacji) oraz obiekt z właściwościami loading i error.
  • handleSubmit: Funkcja obsługująca wysłanie formularza. Pobiera wartości z pól formularza i wywołuje funkcję addBook z obiektem variables zawierającym wartości argumentów mutacji.
  • Wywołanie addBook({ variables: { title, authorId, pages } }) wysyła mutację na serwer.

Korzyści z Używania GraphQL w React

  • Precyzyjne Pobieranie Danych: Pobierasz tylko to, czego potrzebujesz, eliminując over-fetching.
  • Unikanie Wielu Żądań: Pobierasz wszystkie potrzebne dane w jednym zapytaniu, eliminując under-fetching.
  • Silne Typowanie: Schemat GraphQL definiuje strukturę danych, co pomaga unikać błędów i ułatwia debugowanie.
  • Automatyczna Dokumentacja: Narzędzia takie jak GraphiQL umożliwiają eksplorację API i automatyczne generowanie dokumentacji.
  • Lepsza Wydajność: Mniejsze rozmiary odpowiedzi i mniejsza liczba żądań przekładają się na lepszą wydajność aplikacji.
  • Elastyczność: GraphQL daje klientowi większą kontrolę nad danymi, co ułatwia dostosowywanie aplikacji do różnych potrzeb.
  • Łatwiejsze Wprowadzanie Zmian: Zmiany w backendzie rzadziej wymagają zmian w frontendzie, ponieważ frontend samodzielnie wybiera, jakie dane pobiera.

Porównanie GraphQL z REST

CechaRESTGraphQL
Pobieranie danychEndpointy zwracają stałe struktury danychKlient określa, jakie dane chce pobrać
Over-fetchingCzęsteBrak
Under-fetchingCzęsteBrak
Liczba żądańMoże wymagać wielu żądańZazwyczaj jedno żądanie
TypowanieOpcjonalneWymagane
DokumentacjaWymaga oddzielnego narzędziaAutomatyczna
ElastycznośćOgraniczonaWysoka

Najlepsze Praktyki GraphQL w React

  • Używaj Fragmentów: Fragmenty pozwalają na ponowne wykorzystanie fragmentów zapytań w różnych komponentach.

    fragment BookFields on Book {
      id
      title
      author {
        name
      }
    }
    
    query GetBooks {
      books {
        ...BookFields
      }
    }
  • Zarządzaj Stanem Lokalnym: Wykorzystaj możliwości Apollo Clienta do zarządzania stanem lokalnym aplikacji, np. do przechowywania danych użytkownika, filtrów.

  • Optymistyczne Aktualizacje: Stosuj optymistyczne aktualizacje, aby poprawić responsywność aplikacji. Aplikacja natychmiast aktualizuje UI, zakładając, że mutacja zakończy się sukcesem. Jeśli mutacja zakończy się błędem, UI jest cofane do poprzedniego stanu.

  • Paginacja: Implementuj paginację na serwerze i po stronie klienta, aby efektywnie obsługiwać duże zbiory danych.

  • Używaj Narzędzi Developerskich: Korzystaj z narzędzi developerskich Apollo Clienta, aby analizować zapytania, mutacje i cache.

Podsumowanie

GraphQL w połączeniu z Reactem to potężne narzędzie do budowania wydajnych i elastycznych aplikacji webowych. Dzięki precyzyjnemu pobieraniu danych, unikaniu wielu żądań i silnemu typowaniu, GraphQL pozwala na tworzenie aplikacji o lepszej wydajności i mniejszej złożoności. Apollo Client ułatwia integrację GraphQL z Reactem, oferując bogate możliwości zarządzania stanem, cache i optymistycznych aktualizacji.

Zachęcam do samodzielnego eksperymentowania z GraphQL i Apollo Clientem. Stwórz prostą aplikację React pobierającą dane z serwera GraphQL i przekonaj się na własne oczy o korzyściach płynących z tej technologii.

Przydatne Linki

Powodzenia w Twojej przygodzie z GraphQL i Reactem! Mam nadzieję, że ten artykuł był pomocny. Zaglądaj na naszego bloga po więcej artykułów o nowoczesnych technologiach webowych.

Polecane artykuły