Jak używać mutacji GraphQL w React i Apollo Client

Mutacje w GraphQL są odpowiedzialne za zapisywanie danych. Apollo Client oferuje ciekawe mechanizmy, które sprawiają, że praca z mutacjami jest efektywna i przyjemna.

W jednym z moich poprzednich artykułów opisałem zapytania GraphQL. Dzisiaj chciałbym pokazać, jak pracować z mutacjami GraphQL.

Czym jest mutacja w GraphQL?

Mutacja to funkcja, która pozwala modyfikować dane. W interfejsach API REST dane są zazwyczaj pobierane metodą GET i dodawane lub modyfikowane metodą POST lub PUT.

W GraphQL istnieją dwa rodzaje operacji:

  1. Query: pobieranie danych
  2. Mutation: modyfikowanie/dodawanie danych

Przykład mutacji

Spójrz na prostą mutację z GraphQL API platformy Magento eCommerce, która tworzy pusty koszyk:

mutation {
  createEmptyCart(input: {})
}

Mutacja otrzymuje pusty obiekt wejściowy i zwraca token nowo utworzonego koszyka:

Magento GraphQL API createEmptyCart mutation

Spróbujmy czegoś bardziej skomplikowanego – mutacji, która pobiera pewne dane wejściowe za pośrednictwem zmiennych:


mutation {
  addSimpleProductsToCart(input: { cart_id: "B06YCRcXAaOHGTtZ2Iij8SDHAq0AR49F", cart_items: [{ data:{sku: "24-MB01", quantity: 1} }] }) {
    cart {
      total_quantity
      items {
        product {
          name
        }
      }
    }
  }
}

W tym przykładzie można zobaczyć mutację addSimpleProductsToCart, która pobiera obiekt wejściowy z następującymi zmiennymi:

  • cart_id: Identyfikator koszyka do którego produkt zostanie dodany
  • cart_items: tablica obiektów zawierająca SKU produktu i jego ilość. Powyższy przykład dodaje do koszyka jeden produkt SKU 24-MB01o ID B06YCRcXAaOHGTtZ2Iij8SDHAq0AR49F.

Mutacja zwraca obiekt koszyka. W tym przykładzie zażądałem total_quantity koszyka i informacji o nazwach elementów koszyka.

Odpowiedź z serwera GraphQL wygląda następująco:

{
  "data": {
    "addSimpleProductsToCart": {
      "cart": {
        "total_quantity": 1,
        "items": [
          {
            "product": {
              "name": "Joust Duffle Bag"
            }
          }
        ]
      }
    }
  }
}

Hardkodowanie zmiennych nie jest dobrym pomysłem w prawdziwych projektach, więc spójrz na przykład, w którym przekazuję zmienne za pomocą argumentów:

mutation addSimpleProductsToCart($input: AddSimpleProductsToCartInput ) {
  addSimpleProductsToCart(input: $input) {
    cart {
      total_quantity
      items {
        product {
          name
        }
      }
    }
  }
}

W tym przypadku można przekazać zmienne jako obiekt:

{
  "input": {
    "cart_id": "B06YCRcXAaOHGTtZ2Iij8SDHAq0AR49F",
    "cart_items": [
      {
        "data": {
          "sku": "24-MB01",
          "quantity": 1
        }
      }
    ]
  }
}

Mutacja zwraca odpowiedź podobną do poprzedniej:

zmienne graphql w mutacji

Apollo Client

Apollo Client jest jednym z najpopularniejszych klientów GraphQL dla Reacta, a w tym artykule chcę pokazać, jak pracować z mutacjami przy użyciu Apollo Client i Reacta.

Wymagania wstępne

Jeśli chcesz uruchomić poniższe przykłady na swoim komputerze:

  1. Utwórz nową aplikację React za pomocą create-react-app
  2. Zainicjuj klienta Apollo (uwaga: w przykładach użyłem Magento GraphQL API, ale to od ciebie zależy, którego API chcesz użyć).

Wykonanie mutacji w kliencie Apollo

Apollo Client oferuje predefiniowane hooks do wykonywania zapytań i mutacji GraphQL. Hooks te oferują znacznie więcej niż tylko wysyłanie żądań i odbieranie odpowiedzi. Zobaczmy, co zapewnia hook useMutation.

useMutation

Zobaczmy, jak działa hookuseMutation na prawdziwym przykładzie. Aby to zrobić, używam mutacji createEmptyCart. Najpierw muszę zdefiniować tę mutację w kodzie:

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

const CREATE_EMPTY_CART = gql`
  mutation {
    createEmptyCart(input: {})
  }
`;

Ta operacja GraphQL nie przyjmuje żadnych argumentów, więc jej wykonanie jest bardzo proste i wygląda następująco:

useMutation(CREATE_EMPTY_CART)

Pełny przykład:

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

const CREATE_EMPTY_CART = gql`
  mutation {
    createEmptyCart(input: {})
  }
`;

function App() {
  const [mutateFunction, { data, loading, error, called, reset }] = useMutation(CREATE_EMPTY_CART);

  if (loading) {
    return <>Loading...</>
  }

  return <main style={{padding: '10px'}}>
    <h1>GraphQL mutations tutorial</h1>
    <button type="button" onClick={() => mutateFunction()}>Create cart</button>

    <div>
      <h2>Data: </h2>
      {data && data.createEmptyCart}
    </div>
    {error && <div>
      <h2>Error:</h2>
      {error.toString()}
    </div>}
    <div>
      <h2>Called: {called.toString()}</h2>
      <button type="button" onClick={() => reset()}>Reset</button>
    </div>
  </main>
}

export default App;

Ta część kodu:

const [mutateFunction, { data, loading, error }] = useMutation(CREATE_EMPTY_CART); 

Odpowiada za inicjalizację hookauseMutation w komponencie.

Odpowiedź:

Hook useMutation zwraca tablicę zawierającą funkcję mutate i obiekt z pewnymi właściwościami.

mutate function

Funkcja, która musi zostać wywołana w celu wykonania mutacji. Ten przykład został nazwany jako mutateFunction, ale możesz nazwać go jak chcesz. Możesz użyć nazwy operacji: createEmptyCart, lub cokolwiek innego.

Poniższy kod odpowiada za wykonanie mutacji:

<button type="button" onClick={() => mutateFunction()}>Create cart</button>

Zasadniczo istnieje przycisk, który ma powiązaną funkcję mutate na zdarzeniu kliknięcia. Gdy użytkownik kliknie ten przycisk, mutacja zostanie wykonana.

data

Pole data zawiera odpowiedź mutacji. Następnie można użyć tych danych w komponencie. W tym przykładzie wyświetlam odpowiedź mutacji na ekranie:

<div>
  <h2>Data: </h2>
  {data && data.createEmptyCart}
</div>

Loading

Pole loadingpokazuje stan mutacji. Jeśli jest ustawiony na true, oznacza to, że mutacja jest w toku i pobiera dane w danym momencie. Gdy mutacja zwróci wartość i wykonanie zostanie zakończone, pole ładowania będzie równe false.

W tym przykładzie używam pola loading tylko do wyświetlania informacji o ładowaniu na ekranie:

if (loading) {
    return <>Loading...</>
}

error

Gdy mutacja napotka błędy po stronie backendu, zostaną one zwrócone tutaj. Obiekt error może zawierać tablicę obiektów graphQLErrors lub pojedynczy obiekt networkError . Jeśli serwer GraphQL nie generuje żadnego błędu, obiekt błędu jest undefined.

W moim przykładzie wyświetlam potencjalne błędy na ekranie:

{error && <div>
  <h2>Error:</h2>
  {error.toString()}
</div>}

called

Calledjest flagą, która opisuje, czy funkcja mutate jest wywoływana, czy nie.

Reset

Aby przywrócić początkowy stan mutacji, należy wywołać funkcję reset(). Oznacza to, że Apolo przywróci wszystkie pola zwrócone przez hookuseMutation. do wartości domyślnych.

Opcje mutacji

Poprzedni przykład był prosty. Nie przekazałem tam nawet żadnych zmiennych. Zobaczmy coś bardziej wyrafinowanego. Aby przekazać dodatkowe parametry do hooka useMutation, należy przekazać obiekt jako drugi argument operacji.

const [mutateFunction, { data, loading, error, called, reset }] = useMutation(MUTATION_STRING, { 
   // additional options here
});

Uwaga: MUTATION_STRING to ciąg zapytania GraphQL analizowany za pomocą literału szablonu gql .

Zmienne

Obiekt variables umożliwia przekazywanie zmiennych GraphQL do mutacji. Każdy klucz w tym obiekcie reprezentuje jedną zmienną.

const CREATE_EMPTY_CART = gql`
   mutation createCart($input: createEmptyCartInput) {
    createEmptyCart(input: $input)
  }
`;

function App() {
  const [mutateFunction, { data, loading, error, called, reset }] = useMutation(CREATE_EMPTY_CART, {
    variables: {
      input: {
        cart_id: 'lTO8UjTglq5djnpIOseLN7RWvpvwSRba'
      }
    }
  });

 (...) // rest of the code

Zaktualizowałem ciąg mutacji i od teraz akceptuje on jeden argument: obiekt $input:

mutation createCart($input: createEmptyCartInput) {
    createEmptyCart(input: $input)
}

Następnie przekazałem obiekt zmiennych do hooka useMutation:

const [mutateFunction, { data, loading, error, called, reset }] = useMutation(CREATE_EMPTY_CART, {
    variables: {
        input: {
            cart_id: 'lTO8UjTglq5djnpIOseLN7RWvpvwSRba'
        }
    }
});

Ten kod tworzy nowy koszyk z identyfikatorem określonym w zmiennych:

wyniki mutacji graphql

errorPolicy

Pole errorPolicy pozwala określić sposób obsługi błędów przez hook.

Możliwe wartości:

  • None (wartość domyślna) – jeśli odpowiedź zawiera błędy, są one zwracane w obiekcie błędu, a dane są niezdefiniowane.
  • Ignore– błędy są pomijane, więc obiekt błędu nie jest wypełniany, nawet jeśli coś pójdzie nie tak.
  • All – wypełnione są dwa obiekty: dane i błąd. Jest to przydatne, jeśli chcesz użyć częściowych dane, nawet jeśli coś poszło nie tak podczas wykonywania mutacji.

Spójrzmy na przykład z domyślną polityką błędów:

usługa graphql zgłasza błąd

onCompleted

OnCompleted jest funkcją zwrotną wywoływaną, gdy mutacja została wykonana bez błędów. W tej funkcji można uzyskać dostęp do wyników mutacji i opcji przekazanych do mutacji. Spójrz na przykład:

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

const ADD_PRODUCT_TO_CART = gql`
  mutation AddProductsToCart($cartId: String!, $cartItems: [CartItemInput!]!) {
     addProductsToCart(cartId: $cartId, cartItems: $cartItems) {
      cart {
        id
        items {
          quantity
          prices {
            row_total {
              currency
              value
            }
          }
        }
        prices {
          grand_total {
            currency
            value
          }
        }
        total_quantity
      }
    }
  }
`;

const [addProductToCart] = useMutation(ADD_PRODUCT_TO_CART, {
    variables: {
        cartId: 'koESgYi6WU5BiCJDRkgfilDB4z1IsfHV',
        cartItems: [
            {
                selected_options: ['Y29uZmlndXJhYmxlLzE0NC8yMTU'],
                quantity: 1,
                sku: "GC-747-SOCK"
            }
        ]
    },
    onCompleted: (data, options) => {
        console.log(data, options)
    }
});

// (...) 
<button type="button" onClick={() => addProductToCart()}>Add product to cart</button>

W tym przykładzie funkcja onCompleted drukuje dane i opcje na konsoli:

mutation fetch data

onError

onError jest funkcją zwrotną wywoływaną, gdy mutacja zwróci jakiś błąd. W tej funkcji można uzyskać dostęp do obiektu błędu i opcji przekazanych do mutacji.

Aktualizacja cache

Apollo Client posiada własny system buforowania i domyślnie zapisuje dane w pamięci podręcznej. Pamięć podręczna wpływa na szybkość działania, ponieważ niektóre operacje mogą pobierać dane tylko z pamięci podręcznej lub najpierw z pamięci podręcznej, a następnie z serwera.

Ponadto w pracy z mutacjami można wykorzystać technikę zwaną optymistycznymi odpowiedziami. Wyjaśnię to na przykładzie:

  1. Użytkownik wchodzi na stronę internetową
  2. Apollo pobiera listę produktów i wyświetla je na ekranie
  3. Użytkownik dodaje produkt do koszyka
  4. Storefront musi zaktualizować szczegóły koszyka.

Po wykonaniu mutacji mamy dwa sposoby na osiągnięcie tego celu:

  1. Aktualizacja danych koszyka w pamięci podręcznej
  2. Wykonanie zapytanie dotyczące koszyka

Zobaczmy, jak zaimplementować to w Apollo.

Jak Apollo aktualizuje pamięć podręczną

Najpierw utwórzę nowe zapytanie, które pobierze koszyk:

const GET_CART = gql`
  query getCart {
    cart(cart_id: "koESgYi6WU5BiCJDRkgfilDB4z1IsfHV") {
      id
      items {
        quantity
        prices {
          row_total {
            currency
            value
          }
        }
      }
      prices {
        grand_total {
         currency
          value
        }
      }
      total_quantity
    }
  }
`;

Po drugie, użyję hooka useLazyQuery, aby wykonać zapytanie:

const [getCart] = useLazyQuery(GET_CART, {
    onCompleted: (data) => {
      console.log('cart data: ', data)
    }
  });

Po trzecie, dodam przycisk i powiążę funkcję getCart ze zdarzeniem onClick tego przycisku:

<button type="button" onClick={() => getCart()}>Get cart</button>

Gdy użytkownik kliknie ten przycisk, zapytanie jest wykonywane, a wywołanie zwrotne onCompleted wypisuje dane koszyka do konsoli:

dane po stronie serwera operacji graphql drukowane w konsoli

W tym samym czasie Apollo zapisuje dane koszyka w pamięci podręcznej:

Zapytanie zapisane w pamięci podręcznej

Teraz, gdy użytkownik kliknie przycisk Dodaj produkt do koszyka, Apollo uruchomi mutację addProductToCart, a produkt zostanie dodany do koszyka po stronie serwera. Pod maską Apollo aktualizuje dane koszyka w pamięci podręcznej.

Ręczna aktualizacja pamięci podręcznej

W niektórych przypadkach Apollo nie jest w stanie automatycznie zaktualizować pamięci podręcznej. Na szczęście można zdefiniować własną funkcję aktualizacji i ręcznie zapisywać dane w pamięci podręcznej. Funkcję aktualizacji można przekazać jako pole obiektu ustawień, który otrzymuje hook useMutation:

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

const [addProductToCart] = useMutation(ADD_PRODUCT_TO_CART, {
    // (...)
    update(cache, {data: {addProductsToCart> {
        cache.modify({
            fields: {
                cart(existingCartRef = {}) {
                    const newCartRef = cache.writeFragment({
                        data: addProductsToCart.cart,
                        fragment: gql`
                            fragment NewCart on Cart {
                              id
                              items {
                                quantity
                                prices {
                                  row_total {
                                    currency
                                    value
                                  }
                                }
                              }
                              prices {
                                grand_total {
                                  currency
                                  value
                                }
                              }
                              total_quantity
                            }
                          `
                    });
                    return {...existingCartRef, newCartRef};
                }
            }
        });
    }
});

Funkcja update otrzymuje obiekt pamięci podręcznej i dane zwrócone przez mutację:

modify method jest dostępna na obiekcie pamięci podręcznej

Na obiekcie pamięci podręcznej znajduje się funkcja modify, która umożliwia aktualizację pamięci podręcznej. W tej funkcji tworzę nowy fragment koszyka, który zawiera dane zwrócone z mutacji, a na koniec zwracam scalony koszyk z pamięci podręcznej z nowym koszykiem. Następnie Apollo aktualizuje pamięć podręczną.

refetchQueries

Innym mechanizmem aktualizacji stanu po wykonaniu mutacji jest ponowne pobieranie zapytań. Można określić, które zapytania powinny zostać wykonane, przekazując je do tablicy refetchQueries.

const [addProductToCart] = useMutation(ADD_PRODUCT_TO_CART, {
    // (...)
    refetchQueries: [
        'getCart'
    ]
});

„getCart” jest zapytaniem zdefiniowanym wcześniej:

const GET_CART = gql`
  query getCart {
     // (...)
  }
`;

Alternatywnie można przekazać zapytanie jako obiekt w następujący sposób:

const GET_CART = gql`
  query getCart {
     // (...)
  }
`;

// (...)

const [addProductToCart] = useMutation(ADD_PRODUCT_TO_CART, {   
    // (...) 
    refetchQueries: [
        { query: GET_CART}
    ]
});

Optymistyczne odpowiedzi

Kiedy wykonujesz mutacje graphql, modyfikują one dane po stronie serwera, co może zająć trochę czasu. Nowe dane można uzyskać tak szybko, jak to możliwe i wyświetlić je użytkownikowi. Apollo zapewnia interesujący mechanizm: Optimistic mutation results, a dzięki tej funkcji można zaktualizować UI zanim serwer odpowie. Zobacz, jak to działa:

const [addProductToCart] = useMutation(ADD_PRODUCT_TO_CART, {
    // (...)
    optimisticResponse: {
        addProductsToCart: {
            cart: {
                id: 'koESgYi6WU5BiCJDRkgfilDB4z1IsfHV',
                __typename: "Cart",
                total_quantity: 40
                // other cart fields
            }
        }
    }
});

Obiekt optimisticResponse przyjmuje obiekt o takiej samej strukturze jak oczekiwana odpowiedź mutacji. Po wywołaniu funkcji mutate, UI zostanie zaktualizowany. Wreszcie, gdy żądanie zostanie wykonane, Apollo zastępuje fałszywe dane określone przez użytkownika prawdziwymi danymi po stronie serwera.

Podsumowanie

Mutacje obok zapytań są rdzeniem API klienta Apollo. Wysłanie żądania i otrzymanie odpowiedzi to nie wszystko, co oferuje Apollo Client. Dzięki temu, że Apollo ma swój stan, deweloperzy mogą niezawodnie zaprogramować komunikację z API i zapewnić użytkownikom bardzo dobry UX. Jedną z najciekawszych funkcji są optymistyczne odpowiedzi, dzięki którym interfejs użytkownika będzie wydawał się szybki, nawet gdy serwer działa wolno.

Udostępnij post:

Możesz także polubić

Kariera w branży technologicznej: Jak rozwijać swoje umiejętności

Jesteś programistą i chciałbyś się rozwijać? W internecie znajdziesz pełno materiałów o tym, jak to zrobić. Pomimo tego nie uciekaj — mam coś, co Cię zaciekawi. Czy wiesz, że Adam Małysz — legendarny polski skoczek, zanim został mistrzem latania, to był dekarzem? Nie śmiem się porównywać z Panem Adamem, natomiast są dwie rzeczy, które nas łączą.

Ja też byłem dekarzem i też udało mi się przebranżowić. Może nie w tak spektakularny sposób, ale jednak. W tym artykule podzielę się z Tobą moim osobistym doświadczeniem, które zdobyłem na drodze od dekarza przez programistę do tech leada i dam Ci wskazówki, które będziesz mógł zastosować, aby się rozwijać i awansować, a może nawet zmienić diametralnie swoją karierę.

Czytaj więcej
AHA stack przywróćmy prostotę frontendu

AHA! Przywróćmy prostotę Frontendu

Czy zastanawiałeś się, dlaczego w dzisiejszych czasach, gdy mamy dostęp do najnowszych technologii i rozwiązań, projekty IT nadal kończą się fiaskiem? Czy nie uważasz, że w wielu przypadkach zamiast upraszczać to komplikujemy sobie życie i pracę? Czasami mniej znaczy więcej, zwłaszcza w świecie frontendu! Czytaj dalej i dowiedz się czym jest AHA stack i jak robić frontend prościej.

Czytaj więcej