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:
- Query: pobieranie danych
- 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:
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:
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:
- Utwórz nową aplikację React za pomocą create-react-app
- 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:
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:
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:
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:
- Użytkownik wchodzi na stronę internetową
- Apollo pobiera listę produktów i wyświetla je na ekranie
- Użytkownik dodaje produkt do koszyka
- Storefront musi zaktualizować szczegóły koszyka.
Po wykonaniu mutacji mamy dwa sposoby na osiągnięcie tego celu:
- Aktualizacja danych koszyka w pamięci podręcznej
- 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:
W tym samym czasie Apollo zapisuje dane koszyka 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ę:
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.