GraphQL w React #3 – cache, WebSocket i debugowanie w Apollo Client

Ten post jest trzecim z serii o GraphQL:


Spis Treści

  1. Wprowadzenie
  2. Zarządzanie cache w Apollo Client
    1. Cykliczne powtarzanie zapytania do serwera
    2. Ręczna aktualizacja cache
  3. Dwukierunkowa komunikacja z serwerem za pomocą Subscription i WebSocket
    1. Obsługa Subscriptions i WebSocket na serwerze
    2. Obsługa Subscriptions i WebSocket na kliencie
    3. Kod obu aplikacji na Github
  4. Debugowanie za pomocą Apollo Developer Tools
    1. Zakładka GraphQL
    2. Zakładka Queries
    3. Zakładka Mutations
    4. Zakładka Cache
  5. Podsumowanie

Wprowadzenie

Poprzednie dwa posty omówiły podstawy zastosowania GraphQL zarówno na stronie serwera jak i klienta. Możliwości Apollo Client wykraczają jednak daleko poza wysyłanie zapytań na serwer.

Czym więc zajmiemy się w tym wpisie?

  1. Cache’owaniem danych przez Apollo Client
  2. Dwukierunkową komunikacją pomiędzy serwerem a klientem
  3. Debugowaniem za pomocą Apollo Developer Tools

Zmęczenie wadami podejścia REST oraz rosnąca popularność GraphQL powodują, że warto przeczytać ten post i zgłębić wiedzę na temat Apollo Client.

Kod tego posta będzie rozszerzeniem kodu 2 poprzednich: GraphQL w React #1 – poznaj podstawy GraphQL! oraz GraphQL w React #2 – połącz się z serwerem dzięki Apollo Client.


Zarządzanie cache w Apollo Client

W poprzednim poście zdefiniowaliśmy aktualizowane cache poprzez odświeżenie rezultatu zapytania dla komponentu Owners:

const {data, refetch} = useQuery(gql`
      query {
          owners {
              name
          }
      }
  `);
const [createOwner] = useMutation(gql`
    mutation ($name: String!){
      createOwner(owner: {name: $name}) {
        id,
        name,
      }
    }
  `, {onCompleted: refetch});

Oznaczało to, że po każdym wywołaniu funkcji createOwner, które tworzyło na serwerze nowy obiekt owner, cała lista była pobierana ponownie. Nie są to jednak jedyne możliwości aktualizowania cache

Cykliczne powtarzanie zapytania do serwera

Jeżeli zrezygnujemy z użycia funkcji refetch i zastąpimy ją ustawieniem pollInterval na wysyłanym na serwer query:

const {data} = useQuery(gql`
      query {
          owners {
              name
          }
      }
  `, {pollInterval: 30000});

Zauważymy, że co 30 sekund na serwer wysyłane będzie żądanie zwracające obiekty owners. Jeżeli w międzyczasie dodamy jakiś obiekt, nie doda się on do listy od razu – a dopiero po tym, jak zapytanie pobierające listę elementów załaduje nowe dane.

Ręczna aktualizacja cache

Ponieważ w pierwszym poście zdefiniowaliśmy, że żądanie utworzenia obiektu Owner na serwerze zwraca nowo utworzony obiekt:

Mutation: {
    createOwner: (parent, args, context, info) => {
      let owner = {...args.owner, id: owners.length + 1};
      owners.push(owner);
      return owner;
    }
  }

Możemy dzięki tej zwrotce zaktualizować cache bez konieczności odpytywania serwera o całą listę owners:

const loadOwnersQuery = gql`
      query {
          owners {
              name
          }
      }
  `;

  const {data} = useQuery(loadOwnersQuery);

  const [createOwner] = useMutation(gql`
    mutation ($name: String!){
      createOwner(owner: {name: $name}) {
        id,
        name,
      }
    }
  `, {update: (store, request) => {
      const data = store.readQuery({query: loadOwnersQuery})
      const createdOwner = request.data.createOwner
      const updatedOwners = [...data.owners, createdOwner]
      store.writeQuery({ query: loadOwnersQuery, data: {owners: updatedOwners}})
    }});

Przekazujemy w opcjach fukcję update do naszej mutacji. Wewnątrz mamy dostęp do stanu, skąd za pomocą metody readQuery pobieramy aktualne dane. Później za pomocą metody writeQuery wpisujemy dane zaktualizowane o obiekt utworzony na serwerze.


Dwukierunkowa komunikacja z serwerem za pomocą Subscription i WebSocket

Ponieważ w poście dotyczącym części serwerowej zajmowaliśmy się tylko obsługą żądań HTTP, konieczne będą zmiany nie tylko na kliencie, ale również na serwerze.

Obsługa Subscriptions i WebSocket na serwerze

Aby umożliwić publikowanie i subskrybowanie należy zaimportować klasę PubSub i utworzyć jej instancję:

const { ApolloServer, gql, PubSub } = require("apollo-server");

const pubsub = new PubSub();

Należy zmienić również definicję typów:

// inne typy, Mutation dla lepszej orientacji
type Mutation {
  createOwner(owner: OwnerInput!): Owner!
}
  
type Subscription {
    ownerAdded: Owner!
}

Oraz dodać resolver dla typu Subscription:

const resolvers = {
  //inne resolvery
  Subscription: {
    ownerAdded: {
      subscribe: () => pubsub.asyncIterator(['OWNER_ADDED']),
    }
  },
}

Zmodyfikujmy również resolver odpowiadający za dodawanie nowego obiektu typu Owner:

const resolvers = {
  Mutation: {
    createOwner: (parent, args, context, info) => {
      let id = owners.length + 1;
      let owner = {...args.owner, id};
      owners.push(owner);
      pubsub.publish('OWNER_ADDED', {ownerAdded: owner})
      return owner;
    }
  },
}

Podczas dodawania obiektu publikowany jest event, który zawiera świeżo dodany obiekt.

Zmodyfikować powinniśmy również start serwera jak, by móc wyświetlić URL pod którym subskrypcja będzie możliwa:

server.listen()
  .then(({url, subscriptionsUrl}) => {
    console.log(`Sub url: ${subscriptionsUrl}`);
    console.log(`Server url: ${url}`);
  })

Obsługa Subscriptions i WebSocket na kliencie

Zainstalujmy bibliotekę która obsłuży komunikację WebSocket, wymaganą przez Apollo Client:

npm install subscriptions-transport-ws

Następnie, aby móc używać równocześnie komunikacji WebSocket i HTTP, konieczne będzie decydowanie którego adresu należy użyć do realizacji Query które otrzyma instancja klienta Apollo Client:

const httpLink = new HttpLink({
  uri: 'http://localhost:4000/'
});

const wsLink = new WebSocketLink({
  uri: `ws://localhost:4000/graphql`,
  options: {
    reconnect: true
  }
});

const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,
  httpLink,
);

const client = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache()
});

Natomiast do zasubskrybowania do zdefiniowanego na serwerze eventu ownerAdded użyjemy hooka useSubscription:

  const { data: subData } = useSubscription(gql`
      subscription onOwnerAdded {
          ownerAdded {
              id
          }
      }
  `)

  useEffect(() => {
    if (subData?.ownerAdded?.id) {
      alert(`Owner with id: ${subData.ownerAdded.id} added`);
    }
  }, [subData])

Kod obu aplikacji na Github

Kod aplikacji serwera:

https://github.com/radek-anuszewski/graphql-server-example-2

Kod aplikacji klienta:

https://github.com/radek-anuszewski/graphql-client-example-2


Debugowanie za pomocą Apollo Developer Tools

Zainstalujmy oficjalne rozszerzenie do przeglądarki Chrome. Po zainstalowaniu pojawił się dodatkowy tab “Apollo”.

Zakładka GraphQL

W zakładce GraphQL mamy możliwość wysyłania zapytań na serwer, co ważne – po kliknięciu przycisku Explorer możemy wyklikać sobie co serwer ma nam zwrócić:

Zakładka Queries

W zakładce Queries znajduje się historia wszystkich zapytań wysłanych na serwer które pobrały dane. Mamy możliwość bezpośredniego uruchomienia tych zapytań dzięki przyciskowi “Run in GraphQL”:

Zakładka Mutations

W zakładce Mutations znajdują się wszystkie zapytania modyfikujące dane na serwerze:

Zakładka Cache

W zakłace Cache znajdziemy zapisane dane z wysłanych już zapytań:


Podsumowanie

Apollo pozwala nam w prosty obsłużyć zaawansowane przypadki wymagające użycia dwukierunkowej komunikacji między serwerem a klientem czy też obsługę cache. Udostępnia też narzędzia mocno wspomagające debugowanie. Platforma ta będzie doskonałym wyborem implementacji GraphQL w projekcie.