GraphQL w React #2 – połącz się z serwerem dzięki Apollo Client

Ten post jest drugim z serii o GraphQL w React:

Spis Treści

  1. Wprowadzenie
  2. Użycie klienta Apollo Client do załadowania danych z serwera
    1. Wystartowanie serwera
    2. Utworzenie klienta i pobranie danych z serwera
      1. Udostępnienie klienta w aplikacji
      2. Pobranie i wyświetlenie danych za pomocą hooka useQuery i zapisanie danych za pomocą hooka useMutation
        1. Komponent Animals i pochodne – ładowanie danych przez useQuery
        2. Komponent Owners i zapisywanie danych przez useMutation
    3. Kod aplikacji na Github.
    4. Pełny kod aplikacji
  3. Podsumowanie

Wprowadzenie

Ponieważ w poprzednim poście przy pomocy biblioteki Apollo Server postawiliśmy serwer GraphQL, dziś używając Apollo Client stworzymy część kliencką aplikacji. Za jej pomocą pobierzemy dane z serwera.

Użycie klienta Apollo Client do załadowania danych z serwera

Wystartowanie serwera

W poprzednim poście utworzyliśmy następujące API wystawione na zewnątrz:

type Query {
    animals: [Animal]
    owners: [Owner]
    animal(name: String!): Animal
    owner(id: Int!): Owner
}
  
 type Mutation {
  createOwner(owner: OwnerInput!): Owner!
}

Uruchommy więc serwer poleceniem

npm start

Serwer uruchomi się pod adresem http://localhost:4000, ten adres przekażemy do instancji aplikacji klienckiej.

Utworzenie klienta i pobranie danych z serwera

Udostępnienie klienta w aplikacji

Zainstalujmy bibliotekę Apollo Client i GraphQL poleceniem

npm install @apollo/client graphql

Po instalacji nalezy utworzyć instancję klasy ApolloClient:

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

Aby móc korzystać z usług klienta w całej aplikacji za pomocą hooka useQuery, powinniśmy użyć providera ApolloProvider i przekazać mu stworzoną instancję klienta:

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

Pobranie i wyświetlenie danych za pomocą hooka useQuery i zapisanie danych za pomocą hooka useMutation

W naszej aplikacji użyjemy 2 komponentów które będą ładować dane, Animals i Owners:

const App = () => {
  return (
    <div>
      <Animals />
      <Owners />
    </div>
  );
}
Komponent Animals i pochodne – ładowanie danych przez useQuery

Komponent Animals wykorzysta hook useQuery wy wywołać następujące zapytanie:

const {data} = useQuery(gql`
      query {
          animals {
              name
          }
      }
  `);

Jak możemy zauważyć w Dev Tools, na endpoint / zostanie wysłane przekazane w hooku query, które zostanie przez serwer zresolvowane:

W obiekcie data znajduje się pole animals, zawierające listę obiektów z parametrem name. Komponent wyświetla listę elementów, które wewnątrz odpytają o szczegóły konkretnego zwierzęcia:

const Animals = () => {
  const {data} = useQuery(gql`
      query {
          animals {
              name
          }
      }
  `);

  return (
    <p>
      Animals:
      {data?.animals.map(({name}) => (
        <Animal name={name} />
      ))}
    </p>
  )
}

Komponent Animal jest komponentem przechodnim, mającym zapamiętać czy dany obiekt został kliknięty. Nie jest dla nas bardzo istotny (pełny kod będzie dostępny na Githubie), przejdźmy więc od razu do komponentu AnimalData który załaduje dane.

Komponent AnimalData wykorzysta hook useQuery aby załadować dane konkretnego zwierzęcia. W zapytaniu użyjemy variables by móc dynamicznie przekazać dane do zapytania:

const {data} = useQuery(gql`
      query ($name: String!){
          animal(name: $name) {
              name
              birthPlace
          }
      }
  `, {
    variables: {
      name,
    }
  })

Nazwa pola w obiekcie variables musi odpowiadać nazwie zmiennej użytej w kodzie query. Na serwer pod endpoint / wysyłane jest pełne zapytanie wraz ze zmiennymi, którymi ma być ono uzupełnione:

W odpowiedzi w obiekcie data otrzymamy obiekt animal zawierający pola name i birthPlace, które zostaną wyświetlone:

return (
    <p>
      Name: {data.animal.name}
      <br />
      Birth place: {data.animal.birthPlace}
    </p>
  )
Komponent Owners i zapisywanie danych przez useMutation

Ponieważ w komponencie Owners chcemy jednoczesnie pobierać dane i wysyłać je na serwer, hook useQuery poza danymi zwróci również funkcję która ponownie wywoła zapytanie:

const {data, refetch} = useQuery(gql`
      query {
          owners {
              name
          }
      }
  `);

Hook useMutation udostępni nam funkcję która wyśle dane na serwer:

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

W opcjach zdefiniowaliśmy również, że po zapisie na serwerze lista ma zostać odświeżona.

Samo wywołanie funkcji createOwner jest standardowe:

const [name, setName] = useState('') // wypełniany w formularzu

 const onSubmit = e => {
   e.preventDefault()
   createOwner({
     variables: {
       name,
     }
   })
   setName('')
 }

Dane standardowo wysyłane są na endpoint /:

Kod aplikacji na Github

Kod aplikacji można pobrać pod adresem:

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

Pełny kod aplikacji

import React, {useState} from 'react';
import {gql, useMutation, useQuery} from "@apollo/client";

const AnimalData = ({name}) => {
  const {data} = useQuery(gql`
      query ($name: String!){
          animal(name: $name) {
              name
              birthPlace
          }
      }
  `, {
    variables: {
      name,
    }
  })

  if (!data) {
    return null;
  }

  return (
    <p>
      Name: {data.animal.name}
      <br />
      Birth place: {data.animal.birthPlace}
    </p>
  )
}

const Animal = ({name}) => {
  const [clicked, setClicked] = useState(false)

  if (!clicked) {
    return (
      <p>
        <button onClick={() => setClicked(true)}>
          {name}
        </button>
      </p>
    )
  }

  return <AnimalData name={name} />
}

const Animals = () => {
  const {data} = useQuery(gql`
      query {
          animals {
              name
          }
      }
  `);

  return (
    <p>
      Animals:
      {data?.animals.map(({name}) => (
        <Animal name={name} />
      ))}
    </p>
  )
}

const Owners = () => {
  const {data, refetch} = useQuery(gql`
      query {
          owners {
              name
          }
      }
  `);

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

  const [name, setName] = useState('')

  const onSubmit = e => {
    e.preventDefault()
    createOwner({
      variables: {
        name,
      }
    })
    setName('')
  }

  return (
    <p>
      Owners:
      <div>
        {data?.owners.map(el => el.name).join(', ')}
      </div>
      <form onSubmit={onSubmit}>
        <input value={name} onChange={e => setName(e.target.value)} />
        <button type="submit">Submit</button>
      </form>
    </p>
  )
}

const App = () => {
  return (
    <div>
      <Animals />
      <Owners />
    </div>
  );
}

export default App;

Podsumowanie

Platforma Apollo Data Graph pozwala sprawnie zarządzać zarówno częścią serwerową jak i kliencką. Już przykłady zawarte w poście pokazują, że warto zainteresować się technologią GraphQL. W kolejnych postach będą prezentowane bardziej zaawansowane przypadki użycia.