Wprowadzenie do Apollo local state i reactive variables

Apollo Client 3 umożliwia zarządzanie stanem lokalnym poprzez włączenie polityk terenowych i zmiennych reaktywnych. Zobacz, jak zarządzać stanem lokalnym za pomocą klienta Apollo.

W jednym z moich poprzednich artykułów opisałem hook useReducer jako doskonały sposób zarządzania stanem aplikacji React, w tym aplikacji połączonych z API GraphQL za pomocą klienta Apollo.

Zazwyczaj w aplikacji masz stan zdalny (z serwera, z API – mam na myśli GraphQL API), ale możesz też mieć stan lokalny, który nie istnieje po stronie serwera.

Powiedziałem, że useReducer nadaje się do zarządzania takimi sytuacjami – co więcej – za pomocą Apollo Client istnieje inny sposób zarządzania stanem lokalnym.

Apollo Client i State Management

Klient Apollo 3 umożliwia zarządzanie stanem lokalnym poprzez field policies i reactive variables. Field Policy pozwala określić, co się stanie, jeśli zapytasz o określone pola, w tym te, które nie zostały zdefiniowane po stronie serwera GraphQL. Zasady pól definiują pola lokalne, dzięki czemu pola są wypełniane informacjami przechowywanymi w dowolnym miejscu, takimi jak lokalna pamięć masowa i zmienne reaktywne.

Tak więc Apollo Client (wersja >=3) zapewnia dwa mechanizmy obsługi stanu lokalnego:

  • Local-only fields and field policies
  • Reactive variables

Klient Apollo łączy React App i GraphQL API. Ponadto jest biblioteką do zarządzania stanem.

Pomaga połączyć aplikację React z GraphQL API. Dostarcza metody komunikacji z API, mechanizmami cache, helperami itp.

Poza tym klient Apollo zapewnia zintegrowany system zarządzania stanem, który pozwala zarządzać stanem całej aplikacji.

Introduction to the Apollo local state and reactive variables In one of my previous articles, I described the useReducer hook as an excellent way to manage the state of React apps, including apps connected with GraphQL APIs using the apollo client.  Typically in an app, you have a remote state (from sever, for API, here I mean from GraphQL API), but also you can have a local state that does not exist on the server-side.  I talked that useReducer is suitable to manage situations like that – moreover – using Apollo Client, there is another way to manage local state.  Apollo Client for State Management Apollo client 3 enables the management of a local state through incorporating field policies and reactive variables. Field Policy lets you specify what happens if you query specific fields, including those not specified for your GraphQL servers. Field policies define local fields so that the field is populated with information stored anywhere, like local storage and reactive variables.  So  Apollo Client (version >=3) provides two mechanisms to handle local state:  Local-only fields and field policies Reactive variables What is the Apollo client? The Apollo client connects React App, GraphQL API, and besides is a state management library.    It helps you to connect your React App with GraphQL API. It provides methods to communicate with API, cache mechanisms, helpers, etc.   Besides, Apollo client provides an integrated state management system that allows you to manage the state of your whole application.  graphql client  What is Apollo State? Apollo Client has its state management system using GraphQL to communicate directly with external servers and provide scalability.    Apollo Client supports managing the local and remote state of applications, and you will be able to interact with any state on any device with the same API.  Local-only fields and field policies This mechanism allows you to create your client schema. You can extend a server schema or add new fields.  Then, you can define field policies that describe wherefrom data came from. You can use Apollo cache or local storage.  The crucial advantage of this mechanism is using the same API as when you work with server schema.  Local state example If you want to handle local data inside a standard GraphQL query, you have to use a @client directive for local fields:  query getMissions ($limit: Int!){     missions(limit: $limit) {         id         name         twitter         website         wikipedia         links @client // this field is local     } }   Define local state using local-only fields InMemory cache from Apollo Apollo client provides a caching system for local data. Normalized data is saved in memory, and thanks to that, already cached data can get fast.   Field type polices You can read and write to Apollo client cache. Moreover, you can customize how a specific field in your cache is handled. You can specify read, write, and merge functions and add custom logic there.   To define a local state, you need to:  Define field policy and pass it to the InMemoryCache Add field to the query with @client directive Local-only fields tutorial Let's go deeper with the local-only field and check how they work in action.  Initialize project using Create React App  npx create-react-app local-only-fields Install apollo client npm install @apollo/client graphql    Initialize Apollo client Import apollo client stuff in index.js:   import {  ApolloClient,  InMemoryCache,  ApolloProvider, } from "@apollo/client"; Create client instance  const client = new ApolloClient({  uri: 'https://api.spacex.land/graphql/',  cache: new InMemoryCache() });; API.spacex.land/graphql is a fantastic free public demo of GraphQL API, so I use it here. If you want to explore that API, copy the URL to the browser: https://api.spacex.land/graphql/  Connect Apollo with React by wrapping App component with ApolloProvider:   <ApolloProvider client={client}>      <App /> </ApolloProvider> ApolloProvider takes the client argument, which is our already declared Apollo Client. We can use Apollo Client features in the App component and every child component, thanks to that.   The query for missions data Let's get some data from the API. I want to get missions:  query getMissions ($limit: Int!){   missions(limit: $limit) {     id     name     twitter     website     wikipedia   } } Results for this query when I passed 3 as a limit variable:   {   "data": {     "missions": [       {         "id": "9D1B7E0",         "name": "Thaicom",         "twitter": "https://twitter.com/thaicomplc",         "website": "http://www.thaicom.net/en/satellites/overview",         "wikipedia": "https://en.wikipedia.org/wiki/Thaicom"       },       {         "id": "F4F83DE",         "name": "Telstar",         "twitter": null,         "website": "https://www.telesat.com/",         "wikipedia": "https://en.wikipedia.org/wiki/Telesat"       },       {         "id": "F3364BF",         "name": "Iridium NEXT",         "twitter": "https://twitter.com/IridiumBoss?lang=en",         "website": "https://www.iridiumnext.com/",         "wikipedia": "https://en.wikipedia.org/wiki/Iridium_satellite_constellation"       }     ]   } } Let's create a React component that receives that data and, for now, displays the name of the Mission on the screen.  First, create a unit test: src/components/Missions/__tests__/Missions.spec.js  import { render } from "@testing-library/react" import Missions from '../Missions';   describe('Missions component', () => {    it('Should display name of each mission', () => {        const { getByText } = render(<Missions/>);          getByText('Missions component should be here.')      }) }) Of course, the test fails because we event doesn't have a component created yet.   test case of the above example  Add Component: src/components/Missions/Missions.js  import React from "react"   export const Missions = () => {    return <div>        Missions component should be here.    </div> }   export default Missions; Now the test is passing  react components tests   Let's re-export component in src/components/Missions/index.js  export { default } from './Missions'; We need to query for data using the useQuery hook provided by the Apollo client.   In unit tests, you need to have a component wrapped by ApolloProvider. For testing purposes, Apollo provides a unique Provider: MockedProvider, and it allows you to add some mock data. Let's use it.   // src/components/Missions/__tests__/Missions.spec.js  Import MockedProvider:   import { MockedProvider } from '@apollo/client/testing'; Define mocks:   const mocks = [    {        request: {            query: GET_MISSIONS,            variables: {                limit: 3,            },        },        result: {            "data": {                "missions": [                {                    "id": "9D1B7E0",                    "name": "Thaicom",                    "twitter": "https://twitter.com/thaicomplc",                    "website": "http://www.thaicom.net/en/satellites/overview",                    "wikipedia": "https://en.wikipedia.org/wiki/Thaicom"                },                {                    "id": "F4F83DE",                    "name": "Telstar",                    "twitter": null,                    "website": "https://www.telesat.com/",                    "wikipedia": "https://en.wikipedia.org/wiki/Telesat"                },                {                    "id": "F3364BF",                    "name": "Iridium NEXT",                    "twitter": "https://twitter.com/IridiumBoss?lang=en",                    "website": "https://www.iridiumnext.com/",                    "wikipedia": "https://en.wikipedia.org/wiki/Iridium_satellite_constellation"                }                ]            }        }    }, ]; The test fails because we don't have the GET_MISSIONS query defined yet. client side query  Create the file queries/missions.gql.js with the following content:  import { gql } from '@apollo/client';   export const GET_MISSIONS = gql`    query getMissions ($limit: Int!){        missions(limit: $limit) {            id            name            twitter            website            wikipedia        }    }  `;   Import query in the src/components/Missions/__tests__/Missions.spec.js  import { GET_MISSIONS } from "../../../queries/missions.gql"; Now let's wrap the Missions component by the Mocked provider.   const { getByText } = render(    <MockedProvider mocks={mocks}>       <Missions/>    </MockedProvider> ); Now, we can expect that three product missions are visible on the screen because, in our mock response, we have an array with three missions with corresponding names: 'Thaicom,' 'Telstar,' and 'Iridium NEXT.'  To do so, update the test case.  First, make the test case asynchronous by adding the async keyword before the it callback function.  Second, replace the getByText query with the findByText, which works asynchronously.     it('Should display name of each mission', async () => {        const { findByText } = render(            <MockedProvider mocks={mocks}>                <Missions/>            </MockedProvider>        );          await findByText('Thaicom');        await findByText('Telstar');        await findByText('Iridium NEXT');    }); The test fails because we don't query for the data in React component.   By the way, maybe, you think I don't wrap findBytext by the expect…toBe. I do not do that because the findByText query throws an error when it cannot find provided text as an argument, so I don't have to create an assertion because the test will fail if the text is not found.   data returned does not have specified text  Let's update the React component.   First import useQuery hook, and GET_MISSIONS query in src/components/Missions/Missions.js  import { useQuery } from "@apollo/client"; import { GET_MISSIONS } from "../../queries/missions.gql"; Let's query for the data in the component body:  const  { data } = useQuery(GET_MISSIONS, {        variables: {            limit: 3        } }); Now, let's prepare content that Component will render for us. If missions exist, allow's display the name of each Mission. Otherwise, let's show the 'There is no missions' paragraph.  const shouldDisplayMissions = useMemo(() => {     if (data?.missions?.length) {         return data.missions.map(mission => {             return <div key={mission.id}>                 <h2>{mission.name}</h2>             </div>         })     }       return <h2>There are no missions</h2> }, [data]);   In the end, the Component needs to return shouldDisplayMissions:  return shouldDisplayMissions; The full component code:  import React, { useMemo } from "react" import { useQuery } from "@apollo/client"; import { GET_MISSIONS } from "../../queries/missions.gql";   export const Missions = () => {    const  { data } = useQuery(GET_MISSIONS, {        variables: {            limit: 3        }    });      const shouldDisplayMissions = useMemo(() => {        if (data?.missions?.length) {            return data.missions.map(mission => {                return <div key={mission.id}>                    <h2>{mission.name}</h2>                </div>            })        }          return <h2>There are no missions</h2>    }, [data]);      return shouldDisplayMissions; }   export default Missions; Now, the test passed!  state management - final test caseThe last thing for this step is to inject components into the app and see missions in the browser.   // App.js  import Missions from './components/Missions';   function App() {  return <Missions/>; }   export default App;   It works but, initially, it shows, "There are no missions." Let fix it by adding a loading indicator in Missions.js.   First, grab the loading flag from the useQuery hook results:   const  { data, loading } = useQuery(GET_MISSIONS, {     variables: {         limit: 3     } }); Add loading indicator:   if (loading) {     return <p>Loading...</p> }  return shouldDisplayMissions; Besides, add a little bit of styling.   //src/components/Missions/Missions.module.css .mission {    border-bottom: 1px solid black;    padding: 15px; }   Now, import CSS module in Missions.js file:   import classes from './Missions.module.css' and add mission class to the mission div:  return <div key={mission.id} className={classes.mission}>     <h2>{mission.name}</h2> </div>  Here is the result:   local state management - example  Add local-only field OK, so we have data from API. The next task is to display links for the Mission. API returns three fields:   twitter website Wikipedia We can create our local field called: links. It will be an array with links, so we will be able to loop through that array and just display links.   First, let's add a new test case:  it ('Should display links for the mission', async () => {     const localMocks = [         {             ...mocks[0],             result: {                 data: {                     missions: [                         {                             "id": "F4F83DE",                             "name": "Telstar",                             "links": ['https://www.telesat.com/']                         },                     ]                 }             }         }     ]     const { findByText } = render(         <MockedProvider mocks={localMocks}>             <Missions/>         </MockedProvider>     );       await findByText('https://www.telesat.com/"') }); So, we expect that there will be rendered one link: "https://www.telesat.com/"  Define field policy First, we must define the field policy for our local links field.   When you inspect docs for missions query in GraphQL API, you can see that it returns a Mission type:  graphql query   So we need to add a links client field to the Mission type.   To do so, we need to add a configuration to InMeMoryCache in the src/index.js file like this:   const client = new ApolloClient({  uri: 'https://api.spacex.land/graphql/',  cache: new InMemoryCache({    typePolicies: {      Mission: {        fields: {          links: {            read(_, { readField }) {              // logic will be added here in the next step            }          }        }      }    }  }) Now let's return an array with links collected from the Mission. The read function has two arguments. The first one is the field's currently cached value if one exists. The second one is an object that provides several properties and helper functions. We will use the readField function to read other field data.   Our logic for the links local field:  read(_, { readField }) {     const twitter = readField('twiiter');     const wikipedia = readField('wikipedia');     const website = readField('website');     const links = [];       if (twitter) {       links.push(twitter);     }       if (wikipedia) {       links.push(wikipedia);     }       if (website) {       links.push(website);     }       return links;   } The query for local-only field The next step is to include the links field in the query. Let's modify the GET_MISSIONS query:  query getMissions ($limit: Int!){     missions(limit: $limit) {         id         name         twitter         website         wikipedia         links @client     } }  You can define the local-only field by adding the @client directive after the field name.   Display local-only field on the screen We have made good progress, but the test still fails because the Component does not render any links yet.apollo - managing local state - test case  Please update the Missions component by modifying the shouldDisplayMissions Memo function.  const shouldDisplayMissions = useMemo(() => {     if (data?.missions?.length) {         return data.missions.map(mission => {             const shouldDisplayLinks = mission.links?.length ? mission.links.map(link => {                 return <li key={`${mission.id}-${link}`}>                     <a href={link}>{link}</a>                 </li>             }) : null;               return <div key={mission.id} className={classes.mission}>                 <h2>{mission.name}</h2>                 {shouldDisplayLinks}             </div>         })     }       return <h2>There are no missions</h2> }, [data]); We are good now. Everything work as well in the browser:  local and server data in action  And tests pass:  managing local state - all tests pass  Working Demo Here you can see the demo of the app:  https://apollo-client-local-only-fields-tutorial.vercel.app/  Source code Here you can find the source code for this tutorial: https://github.com/Frodigo/apollo-client-local-only-fields-tutorial  Here are the commits for each step:  Initialize project using Create React App Install Apollo Client Initialize Apollo Client The query for missions data Add local-only field   Reactive variables OK, you met local-only fields, and now let's take a look at another mechanism called: Reactive variables.  You can write and read data anywhere in your app using reactive variables.  Apollo client doesn't store reactive variables in its cache, so you don't have to keep the strict structure of cached data.  Apollo client detects changes of reactive variables, and when the value changes, a variable is automatically updated in all places.  Reactive variables in action This time I would like to show you the case of using reactive variables. I don't want to repeat Apollo docs, so you can see the basics of reading and modifying reactive variables here.  The case I've started work on the cart and mini-cart in my react-apollo-storefront app. The first thing that I needed to do was create an empty cart.  In Magento GraphQL API, there is the mutation createEmptyCart. That mutation returns the cart ID.  I wanted to get a cart ID, store it in my app, and after the page, refresh check if a value exists in the local state and if yes, get it from it without running mutation.  apollo local state management - example case to use  Implementation First, let's define the reactive variable:   import { makeVar } from "@apollo/client";  export const CART_ID_IDENTIFER = 'currentCardId' export const cartId = makeVar(localStorage.getItem(CART_ID_IDENTIFER)); Second, use that variable in a component, context, or hook and make it reactive:  import { useReactiveVar } from '@apollo/client';  export const CartProvider = ({ children }) => {     const currentCartId = useReactiveVar(cartId); }; Third, define the mutation to collect a cart Id from the server:  // mutation:  import { gql } from '@apollo/client';  export const CREATE_EMPTY_CART = gql`     mutation createEmptyCart {         createEmptyCart     } `;  // component // imports import { useMutation } from '@apollo/client'; import { CREATE_EMPTY_CART } from '../../mutations/cart.gql'  // ... in component body const [ createEmptyCart ] = useMutation(CREATE_EMPTY_CART, {     update(_, { data: { createEmptyCart } }) {         cartId(createEmptyCart);         localStorage.setItem(CART_ID_IDENTIFER, createEmptyCart);         } }); I used the update callback there, and I updated the reactive variable:  cartId(createEmptyCart) Then I also updated a value in the local storage.  Last, check if cart ID exists in the local state and if not, send the mutation to a sever.  const currentCartId = useReactiveVar(cartId);  useEffect(() => {     if (!currentCartId) {         createEmptyCart();     } }, [createEmptyCart, currentCartId]); Summary Today I showed you two techniques of managing local data in Apollo. Local-only fields, and reactive variables. Those mechanism provides a lot of flexibility, and and they should be consider when you architecting state management in your React application.

Co to jest Apollo State?

Apollo Client ma swój system zarządzania stanem wykorzystujący GraphQL do bezpośredniej komunikacji z zewnętrznymi serwerami i zapewnia skalowalność.

Apollo Client obsługuje zarządzanie lokalnym i zdalnym stanem aplikacji, dzięki czemu będziesz mógł wchodzić w interakcje z dowolnym stanem na dowolnym urządzeniu z tym samym interfejsem API.

Local-only fields i field policies

Ten mechanizm umożliwia utworzenie schematu klienta. Możesz rozszerzyć schemat serwera lub dodać nowe pola.

Następnie możesz zdefiniować field policies, które opisują, skąd pochodzą dane. Możesz użyć pamięci podręcznej Apollo lub pamięci lokalnej.

Kluczową zaletą tego mechanizmu jest korzystanie z tego samego API, co przy pracy ze schematem serwera.

Local state: przykład

Jeśli chcesz obsługiwać dane lokalne w standardowym zapytaniu GraphQL, musisz użyć dyrektywy @client dla pól lokalnych:

query getMissions ($limit: Int!){
    missions(limit: $limit) {
        id
        name
        twitter
        website
        wikipedia
        links @client // this field is local
    }
}  

Definicja local state przy użyciu local-only fields

Apollo InMemory cache

Klient Apollo zapewnia system buforowania danych lokalnych. Znormalizowane dane są zapisywane w pamięci, dzięki czemu dane już zapisane w pamięci podręcznej mogą być szybko pobierane.

Field type policies

Możesz odczytywać i zapisywać w pamięci podręcznej klienta Apollo. Ponadto możesz dostosować sposób obsługi określonego pola w pamięci podręcznej. Możesz określić funkcje odczytu, zapisu i scalania oraz dodać logikę niestandardową.

Aby zdefiniować stan lokalny, należy:

  1. Zdefiniować zasady pola i przekaż je do InMemoryCache
  2. Dodać zdefiniowane wcześniej pole do query używając dyrektywy @client

Local-only fields tutorial

Wejdźmy głębiej w pole tylko lokalne i sprawdźmy, jak działają w akcji.

Zainicjuj projekt za pomocą aplikacji Create React

$ npx create-react-app local-only-fields

Zainstaluj apollo client

$ npm install @apollo/client graphql   

Zainicjuj Apollo client

Import apollo client w index.js: 

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

Utwórz instancję klienta:

const client = new ApolloClient({
 uri: 'https://api.spacex.land/graphql/',
 cache: new InMemoryCache()
});;

API.spacex.land/graphql to fantastyczna bezpłatna publiczna wersja demonstracyjna GraphQL API, więc używam jej tutaj. Jeśli chcesz eksplorować ten interfejs API, skopiuj adres URL do przeglądarki: https://api.spacex.land/graphql/

Połącz Apollo z Reactem, opakowując komponent App w ApolloProvider:

<ApolloProvider client={client}>
     <App />
</ApolloProvider>

ApolloProvider przyjmuje argument klienta, którym jest nasz już zadeklarowany klient Apollo. Dzięki temu możemy korzystać z funkcji Apollo Client w komponencie App i każdym komponencie potomnym.

Zapytanie po dane misji

Pobierzmy dane z API. Chcę pobrać misje:

query getMissions ($limit: Int!){
  missions(limit: $limit) {
    id
    name
    twitter
    website
    wikipedia
  }
}

Wyniki dla tego zapytania, gdy przekazałem 3 jako zmienną graniczną:

{
  "data": {
    "missions": [
      {
        "id": "9D1B7E0",
        "name": "Thaicom",
        "twitter": "https://twitter.com/thaicomplc",
        "website": "http://www.thaicom.net/en/satellites/overview",
        "wikipedia": "https://en.wikipedia.org/wiki/Thaicom"
      },
      {
        "id": "F4F83DE",
        "name": "Telstar",
        "twitter": null,
        "website": "https://www.telesat.com/",
        "wikipedia": "https://en.wikipedia.org/wiki/Telesat"
      },
      {
        "id": "F3364BF",
        "name": "Iridium NEXT",
        "twitter": "https://twitter.com/IridiumBoss?lang=en",
        "website": "https://www.iridiumnext.com/",
        "wikipedia": "https://en.wikipedia.org/wiki/Iridium_satellite_constellation"
      }
    ]
  }
}

Stwórzmy komponent React, który odbiera te dane i na razie wyświetla nazwę Misji na ekranie.

Najpierw utwórz test jednostkowy: src/components/Missions/tests/Missions.spec.js

import { render } from "@testing-library/react"
import Missions from '../Missions';
 
describe('Missions component', () => {
   it('Should display name of each mission', () => {
       const { getByText } = render(<Missions/>);
 
       getByText('Missions component should be here.')
 
   })
})

Oczywiście test kończy się niepowodzeniem, ponieważ nie mamy jeszcze utworzonego komponentu.

Dodaj komponent: src/components/Missions/Missions.js

import React from "react"
 
export const Missions = () => {
   return <div>
       Missions component should be here.
   </div>
}
 
export default Missions;

Teraz tst przechodzi:

Re-exportuj komponent w src/components/Missions/index.js

export { default } from './Missions';

Musimy zapytać o dane za pomocą haka useQuery dostarczonego przez klienta Apollo.

W testach jednostkowych musisz mieć komponent opakowany przez ApolloProvider. Do celów testowych Apollo zapewnia unikalnego dostawcę: MockedProvider i umożliwia dodanie fałszywych danych. Użyjmy tego.

// src/components/Missions/__tests__/Missions.spec.js

import MockedProvider:
 
import { MockedProvider } from '@apollo/client/testing';

Zdefiniuj mocki:

const mocks = [
   {
       request: {
           query: GET_MISSIONS,
           variables: {
               limit: 3,
           },
       },
       result: {
           "data": {
               "missions": [
               {
                   "id": "9D1B7E0",
                   "name": "Thaicom",
                   "twitter": "https://twitter.com/thaicomplc",
                   "website": "http://www.thaicom.net/en/satellites/overview",
                   "wikipedia": "https://en.wikipedia.org/wiki/Thaicom"
               },
               {
                   "id": "F4F83DE",
                   "name": "Telstar",
                   "twitter": null,
                   "website": "https://www.telesat.com/",
                   "wikipedia": "https://en.wikipedia.org/wiki/Telesat"
               },
               {
                   "id": "F3364BF",
                   "name": "Iridium NEXT",
                   "twitter": "https://twitter.com/IridiumBoss?lang=en",
                   "website": "https://www.iridiumnext.com/",
                   "wikipedia": "https://en.wikipedia.org/wiki/Iridium_satellite_constellation"
               }
               ]
           }
       }
   },
];

Test kończy się niepowodzeniem, ponieważ nie mamy jeszcze zdefiniowanego zapytania GET_MISSIONS.

client side query

Utwórz plik query/missions.gql.js:

import { gql } from '@apollo/client';
 
export const GET_MISSIONS = gql`
   query getMissions ($limit: Int!){
       missions(limit: $limit) {
           id
           name
           twitter
           website
           wikipedia
       }
   } 
`;
 

Zaimportuj query w src/components/Missions/__tests__/Missions.spec.js

import { GET_MISSIONS } from "../../../queries/missions.gql";

Teraz otocz komponent Missions dostawcą Mocked.

const { getByText } = render(
   <MockedProvider mocks={mocks}>
      <Missions/>
   </MockedProvider>
);

Teraz możemy spodziewać się, że trzy misje produktowe będą widoczne na ekranie, ponieważ w naszej próbnej odpowiedzi mamy tablicę z trzema misjami o odpowiednich nazwach: „Thaicom”, „Telstar” i „Iridium NEXT”.

W tym celu zaktualizuj przypadek testowy.

Najpierw ustaw przypadek testowy jako asynchroniczny, dodając słowo kluczowe async przed funkcją wywołania zwrotnego it.

Po drugie, zastąp zapytanie getByText zapytaniem findByText, które działa asynchronicznie.

   it('Should display name of each mission', async () => {
       const { findByText } = render(
           <MockedProvider mocks={mocks}>
               <Missions/>
           </MockedProvider>
       );
 
       await findByText('Thaicom');
       await findByText('Telstar');
       await findByText('Iridium NEXT');
   });

Test kończy się niepowodzeniem, ponieważ nie pytamy o dane w komponencie React.

Przy okazji, może myślisz, że nie owijam findBytext przez expect…toBe. Nie robię tego, ponieważ zapytanie findByText zgłasza błąd, gdy nie może znaleźć podanego tekstu jako argumentu, więc nie muszę tworzyć asercji, ponieważ test zakończy się niepowodzeniem, jeśli tekst nie zostanie znaleziony.

data returned does not have specified text

Zaktualizujmy komponent React.

Najpierw zaimportuj hak useQuery i zapytanie GET_MISSIONS w src/components/Missions/Missions.js

import { useQuery } from "@apollo/client";
import { GET_MISSIONS } from "../../queries/missions.gql";

Zapytajmy o dane w komponencie:

const  { data } = useQuery(GET_MISSIONS, {
       variables: {
           limit: 3
       }
});

Teraz przygotujmy treść, którą komponent wyrenderuje dla nas. Jeśli istnieją misje, pozwól wyświetlić nazwę każdej misji. W przeciwnym razie pokażmy akapit „Nie ma misji”.

const shouldDisplayMissions = useMemo(() => {
    if (data?.missions?.length) {
        return data.missions.map(mission => {
            return <div key={mission.id}>
                <h2>{mission.name}</h2>
            </div>
        })
    }


    return <h2>There are no missions</h2>
}, [data]);
 

Komponent musi zwrócić shouldDisplayMissions:

import React, { useMemo } from "react"
import { useQuery } from "@apollo/client";
import { GET_MISSIONS } from "../../queries/missions.gql";
 
export const Missions = () => {
   const  { data } = useQuery(GET_MISSIONS, {
       variables: {
           limit: 3
       }
   });
 
   const shouldDisplayMissions = useMemo(() => {
       if (data?.missions?.length) {
           return data.missions.map(mission => {
               return <div key={mission.id}>
                   <h2>{mission.name}</h2>
               </div>
           })
       }
 
       return <h2>There are no missions</h2>
   }, [data]);
 
   return shouldDisplayMissions;
}
 
export default Missions;

Teraz test przechodzi!

data returned does not have specified text

Ostatnią rzeczą w tym kroku jest wstrzyknięcie komponentów do aplikacji i zobaczenie misji w przeglądarce.

// App.js

import Missions from './components/Missions';
 
function App() {
 return <Missions/>;
}
 
export default App;

Działa, ale początkowo pokazuje komunikat „Brak misji”. Naprawmy to, dodając wskaźnik loading w pliku Missions.js.

Najpierw pobierz flagę loading z wyników haka useQuery:

const  { data, loading } = useQuery(GET_MISSIONS, {
    variables: {
        limit: 3
    }
});

Dodaj loading indicator: 

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

return shouldDisplayMissions;

Poza tym dodaj trochę stylizacji.

//src/components/Missions/Missions.module.css
.mission {
   border-bottom: 1px solid black;
   padding: 15px;
}

Teraz zaimportuj moduł CSS w pliku Missions.js:

import classes from './Missions.module.css'

i dodaj klasę misji do div misji:

return <div key={mission.id} className={classes.mission}>
    <h2>{mission.name}</h2>
</div>

Rezultat naszego działania:

data returned does not have specified text

Dodawanie local-only field

OK, więc mamy dane z API. Kolejnym zadaniem jest wyświetlenie linków do Misji. API zwraca trzy pola:

  • twitter
  • website
  • Wikipedia

Możemy stworzyć nasze lokalne pole o nazwie: links. Będzie to tablica z łączami, więc możemy przejść przez tę tablicę i po prostu wyświetlić łącza.

Najpierw dodajmy nowy przypadek testowy:

it ('Should display links for the mission', async () => {
    const localMocks = [
        {
            ...mocks[0],
            result: {
                data: {
                    missions: [
                        {
                            "id": "F4F83DE",
                            "name": "Telstar",
                            "links": ['https://www.telesat.com/']
                        },
                    ]
                }
            }
        }
    ]
    const { findByText } = render(
        <MockedProvider mocks={localMocks}>
            <Missions/>
        </MockedProvider>
    );


    await findByText('https://www.telesat.com/"')
});

Spodziewamy się więc, że zostanie wyrenderowany jeden link: „https://www.telesat.com/

Definiowanie field policy

Po pierwsze, musimy zdefiniować politykę pola dla naszego pola linków lokalnych.

Gdy sprawdzisz dokumentację doczyczącą zapytania misji w GraphQL API, zobaczysz, że swraca ono tablicę z obiektami Mission.

data returned does not have specified text

Musimy więc dodać pole lokalne o nazwie links do typu misji.

Aby to zrobić, musimy dodać konfigurację do InMeMoryCache w pliku src/index.js w następujący sposób:

const client = new ApolloClient({
 uri: 'https://api.spacex.land/graphql/',
 cache: new InMemoryCache({
   typePolicies: {
     Mission: {
       fields: {
         links: {
           read(_, { readField }) {
             // logic will be added here in the next step
           }
         }
       }
     }
   }
 })

Teraz zwróćmy tablicę z linkami zebranymi z misji. Funkcja read ma dwa argumenty. Pierwsza z nich to aktualnie przechowywana w pamięci podręcznej wartość pola, jeśli taka istnieje. Drugi to obiekt, który udostępnia kilka właściwości i funkcji pomocniczych. Użyjemy funkcji readField do odczytania innych danych pola.

Nasza logika dla pola lokalnego linków:

read(_, { readField }) {
    const twitter = readField('twiiter');
    const wikipedia = readField('wikipedia');
    const website = readField('website');
    const links = [];


    if (twitter) {
      links.push(twitter);
    }


    if (wikipedia) {
      links.push(wikipedia);
    }


    if (website) {
      links.push(website);
    }


    return links;
  }

Zapytanie po local-only field

Kolejnym krokiem jest uwzględnienie pola linków w zapytaniu. Zmodyfikujmy zapytanie GET_MISSIONS:

query getMissions ($limit: Int!){
    missions(limit: $limit) {
        id
        name
        twitter
        website
        wikipedia
        links @client
    }
} 

Możesz zdefiniować pole tylko lokalne, dodając dyrektywę @client po nazwie pola.

Wyświetlanie local-only field na ekranie

Poczyniliśmy duże postępy, ale test nadal kończy się niepowodzeniem, ponieważ komponent nie renderuje jeszcze żadnych linków.

Zaktualizuj komponent Missions, modyfikując funkcję shouldDisplayMissions Memo.

const shouldDisplayMissions = useMemo(() => {
    if (data?.missions?.length) {
        return data.missions.map(mission => {
            const shouldDisplayLinks = mission.links?.length ? mission.links.map(link => {
                return <li key={`${mission.id}-${link}`}>
                    <a href={link}>{link}</a>
                </li>
            }) : null;


            return <div key={mission.id} className={classes.mission}>
                <h2>{mission.name}</h2>
                {shouldDisplayLinks}
            </div>
        })
    }


    return <h2>There are no missions</h2>
}, [data]);

Jestest super! Wszystko działa równie dobrze w przeglądarce:

testy przechodzą:

DziałająceDemo

Here you can see the demo of the app:

https://apollo-client-local-only-fields-tutorial.vercel.app/

Kod źródłowy

Tutaj możesz znaleźć kod źródłowy tego samouczka: https://github.com/Frodigo/apollo-client-local-only-fields-tutorial

A tu commity dla każdego kroku:

  1. Initialize the project using Create React App
  2. Install Apollo Client
  3. Initialize Apollo Client
  4. The query for missions data
  5. Add local-only field

Reactive variables

OK, poznałeś pola tylko lokalne, a teraz spójrzmy na inny mechanizm o nazwie: Zmienne reaktywne.

Możesz zapisywać i odczytywać dane w dowolnym miejscu w swojej aplikacji przy użyciu zmiennych reaktywnych.

Apollo nie przechowuje reaktywnych zmiennych w swojej pamięci podręcznej, więc nie musisz utrzymywać ścisłej struktury danych w pamięci podręcznej.

Klient Apollo wykrywa zmiany w zmiennych reaktywnych, a gdy zmienia się wartość, zmienna jest automatycznie aktualizowana we wszystkich miejscach.

Reactive variables w akcji

Tym razem chciałbym pokazać przypadek z wykorzystaniem zmiennych reaktywnych. Nie chcę powtarzać dokumentów Apollo, więc możesz zobaczyć podstawy czytania i modyfikowania zmiennych reaktywnych tutaj.

Przypadek użycia

W Magento GraphQL API istnieje mutacja createEmptyCart. Ta mutacja zwraca identyfikator wózka.

Rozpocząłem pracę nad wózkiem i miniwózkiem w mojej aplikacji React-Apollo-Storefront. Pierwszą rzeczą, którą musiałem zrobić, było utworzenie pustego koszyka.

Chciałem uzyskać identyfikator koszyka, zapisać go w mojej aplikacji, a po odświeżeniu strony sprawdzić, czy wartość istnieje w stanie lokalnym, a jeśli tak, pobrać ją z niego bez uruchamiania mutacji.

apollo local state manement - example case to use

Implementacje

Po pierwsze, zdefiniuj zmienną reaktywną:

import { makeVar } from "@apollo/client";

export const CART_ID_IDENTIFER = 'currentCardId'
export const cartId = makeVar(localStorage.getItem(CART_ID_IDENTIFER));

Po drugie, użyj tej zmiennej w komponencie, kontekście lub hookui spraw, aby była reaktywna:

import { useReactiveVar } from '@apollo/client';

export const CartProvider = ({ children }) => {
    const currentCartId = useReactiveVar(cartId);
};

po trzecie, zdefiniuj mutację, aby pobrać identyfikator koszyka z serwera:

// mutation: 
import { gql } from '@apollo/client';

export const CREATE_EMPTY_CART = gql`
    mutation createEmptyCart {
        createEmptyCart
    }
`;

// component
// imports
import { useMutation } from '@apollo/client';
import { CREATE_EMPTY_CART } from '../../mutations/cart.gql'

// ... in component body
const [ createEmptyCart ] = useMutation(CREATE_EMPTY_CART, {
    update(_, { data: { createEmptyCart } }) {
        cartId(createEmptyCart);
        localStorage.setItem(CART_ID_IDENTIFER, createEmptyCart);
        }
});

użyłem tam wywołania zwrotnego update i zaktualizowałem zmienną reaktywną:

cartId(createEmptyCart)

Następnie zaktualizowałem również wartość w pamięci lokalnej.

Na koniec sprawdzam, czy identyfikator wózka istnieje w stanie lokalnym, a jeśli nie, wyślij mutację do serwera.

const currentCartId = useReactiveVar(cartId);

useEffect(() => {
    if (!currentCartId) {
        createEmptyCart();
    }
}, [createEmptyCart, currentCartId]);

Podsumowanie

Dzisiaj pokazałem Cidwie techniki zarządzania lokalnymi danymi w Apollo. Pola tylko lokalne i zmienne reaktywne. Mechanizmy te zapewniają dużą elastyczność i należy je wziąć pod uwagę podczas projektowania architektury zarządzania stanem w aplikacji React. Dodatkowo polecam poczytać o wyszydzaniu zapytań i mutacji GraphQL.

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