DEV Community

Cover image for GraphQL no front-end (React e Apollo)
Ivan Trindade
Ivan Trindade

Posted on

GraphQL no front-end (React e Apollo)

Na última década, tecnologias como GraphQL mudaram a forma como construímos aplicações web e como elas se comunicam. O GraphQL oferece certos benefícios em relação ás APIs REST — vamos descobrir quais são.


Um dos principais benefícios do GraphQL, é a capacidade do cliente solicitar o que precisa do servidor e receber esses dados de forma exata e previsível. Sem muito esforço, pode=se extrair facilmente dados aninhados apenas adicionando mais propriedades ás nossas consultas, em vez de adicionar vários pontos de extremidade. Isso evita problemas como busca excessiva que podem afetar o desempenho.

Normalmente, para lidar com o GraphQL no lado do cliente, usamos o Apollo Client. Ele permite que os desenvolvedores definam, manipulem e disponibilizem consultas/multações em nossa aplicação. Ele também pode atuar como uma ferramenta de gerenciamento de estado em sua aplicação do lado do cliente.

Neste artigo, aprenderemos como lidar com atualizações em tempo real no lado do cliente usando GraphQL. Aprenderemos como fazer isso com os recursos do GraphQL, como atualização de cache, assinaturas e intrerface do usuário otimista. Também abordaremos como usar o Apollo como uma ferramenta de gerenciamento de estado, possivelmente substituindo o redux. Além disso, veremos como criar consultas GraphQL usáveis com Fragments e como usar diretivas Apollo para escrever consultas mais complexas.

Instalação

Antes de começarmos, vamos apenas passar pela instalação e configurar nosso projeto. Vamos direto ao código. Para criar uma aplicação React, certifique-se de ter o Node.js instalado em seu computador.

Vamos começar com nossa aplicação React executando este comando:

npx create-react-app react-graphql
Enter fullscreen mode Exit fullscreen mode

Em seguida, vamos navegar para a pasta do nosso projeto no terminal:

cd react-graphql
Enter fullscreen mode Exit fullscreen mode

Feito isso, instalaremos o Apollo usando esta linha:

npm i @apollo/client
Enter fullscreen mode Exit fullscreen mode

Atualização em tempo real no GraphQL

A capacidade de criar uma atualização em tempo real no lado do cliente, ajuda a melhorar a experiência do usuário no site, fazendo com que tudo pareça mais tranquilo. Imagine uma situação em que um usuário adiciona um novo item preenchendo um formulário e esse item é atualizando instantaneamente por ser adicionado á lista de itens na mesma página. Embora essa atualização em tempo real possa ser sincronizada com um servidor diretamente por meio de assinaturas, ou poder ser manipulada no front-end por meio de coisas como Optimistic UI, ou usando a função update no useMutation. Então, vamos para a implementação técnica.

Atualizando o cache diretamente usando a função update no useMutation

useMutations são importados diretamente da biblioteca @apollo/client e nos ajudam a fazer mutações nos dados em nosso servidor.

Normalmente, podemos criar mutações com Apollo usando useMutations, mas além disso, o que faremos é usar a função update para atualizar nosso cache do cliente apollo, diretamente através de useMutation.

Neste exemplo abaixo, enviamos consultas ao servidor para obter uma lista de pets usando useQuery e fazemos uma mutação tendo um formulário para adicionar mais pets ao nosso servidor usando useMutation. O problema que teremos é que quando um novo animal de estimação é adicionado ao servidor, ele não é adicionado à lista de animais de estimação (no navegador) imediatamente, a menos que a página seja atualizada. Isso faz com que a experiência do usuário nesta seção da aplicação pareça quebrada, especialmente porque a lista de animais de estimação e o formulário estão na mesma página. Você pode visualizar o exemplo no vimeo.

import React, { useState } from "react";
import gql from "graphql-tag";
import { useQuery, useMutation } from "@apollo/client";
import Loader from "../components/Loader";
import PetSection from "../components/PetSection";

//ALL_PETS usa gql de @apollo/client para nos permitir enviar consultas aninhadas

const ALL_PETS = gql`
  query AllPets {
    pets {
      id
      name
      type
      img
    }
  }
`;

// NEW_PET usa gql de @apollo/client para criar mutations
const NEW_PET = gql`
  mutation CreateAPet($newPet: NewPetInput!) {
    addedPet(input: $newPet) {
      id
      name
      type
      img
    }
  }
`;
function Pets() {
  const initialCount = 0;
  const [count, setCount] = useState(initialCount);
  const pets = useQuery(ALL_PETS);
  const [createPet, newPet] = useMutation(NEW_PET);
  const [name, setName] = useState("");
  const type = `DOG`;

  const onSubmit = (input) => {
    createPet({
      variables: { newPet: input },
    });
  };

  // esta função aciona a ação de envio chamando a função onSubmit acima dela
  const submit = (e) => {
    e.preventDefault();
    onSubmit({ name, type });
  };

// Se os dados estiverem carregando, exibimos o componente <Loader/>instead
  if (pets.loading || newPet.loading) {
    return <Loader />;
  }

// percorre os dados dos animais de estimação para obter cada animal de estimação e exibi-los com props usando o componente <PetSection>.
  const petsList = pets.data.pets.map((pet) => (
    <div className="col-xs-12 col-md-4 col" key={pet.id}>
      <div className="box">
        <PetSection pet={pet} />
      </div>
    </div>
  ));

  return (
    <div>
      <form onSubmit={submit}>
        <input
          className="input"
          type="text"
          placeholder="pet name"
          value={name}
          onChange={(e) => setName(e.target.value)}
          required
        />
        <button type="submit" name="submit">
          add pet
        </button>
      </form>
      <div>
        {petsList}
      </div>

    </div>
  );
}
export default Pets;

Enter fullscreen mode Exit fullscreen mode

Usar a função update no hook useMutation, nos permite atualizar diretamente nosso cache lendo e gravando nosso arquivo ALL_PETS. Imediatamente pressionamos o botão enviar, os dados são adicionados á lista de animais de estimação no cache, alterando ALL_PETS. Isso nos permite atualizar nosso cache do lado do cliente imediatamente com dados consistentes.

import React, { useState } from "react";
import gql from "graphql-tag";
import { useQuery, useMutation } from "@apollo/client";
import Loader from "../components/Loader";
import PetSection from "../components/PetSection";

//ALL_PETS usa gql de @apollo/client para nos permitir enviar consultas aninhadas
const ALL_PETS = gql`
  query AllPets {
    pets {
      id
      name
      type
      img
    }
  }
`;

// NEW_PET usa gql de @apollo/client para criar mutations
const NEW_PET = gql`
  mutation CreateAPet($newPet: NewPetInput!) {
    addedPet(input: $newPet) {
      id
      name
      type
      img
    }
  }
`;

function ThePets() {
  const initialCount = 0;
  const [count, setCount] = useState(initialCount);
  const pets = useQuery(ALL_PETS);

  //Em seguida, usamos useMutation e update() para atualizar nosso ALL_PET

  const [createPet, newPet] = useMutation(NEW_PET, {
    update(cache, {data: {addedPet}}) {
      const allPets = cache.readQuery({query: ALL_PETS})
      cache.writeQuery({
        query: ALL_PETS,
        data: {pets: [addedPet, ...allPets.pets]}
      })
    }
  });
  const [name, setName] = useState("");
  const type = `DOG`;

  const onSubmit = (input) => {
    createPet({
      variables: { newPet: input },
    });
  };

  //Lida com o envio de Pets que eventualmente aciona createPet por meio de onSumit

  const submit = (e) => {
    e.preventDefault();
    onSubmit({ name, type });
  };

  //Se os dados estiverem carregando, exibimos o componente <Loader/>

  if (pets.loading || newPet.loading) {
    return <Loader />;
  }

//percorre os dados dos animais de estimação para obter cada animal de estimação e exibi-los com adereços usando o componente <PetSection>

  const petsList = pets.data.pets.map((pet) => (
    <div className="col-xs-12 col-md-4 col" key={pet.id}>
      <div className="box">
        <PetSection pet={pet} />
      </div>
    </div>
  ));
  return (
    <div>
      <form onSubmit={submit}>
        <input
          className="input"
          type="text"
          placeholder="pet name"
          value={name}
          onChange={(e) => setName(e.target.value)}
          required
        />
        <button type="submit" name="submit">
          add pet
        </button>
      </form>
      <div>
        {petsList}
      </div>

    </div>
  );
}
export default ThePets;

Enter fullscreen mode Exit fullscreen mode

Você pode visualizar o resultado no vimeo.

Assinaturas no GraphQL

Com base nas funcionalidades, a assinatura no GraphQL é semelhante ás consultas. A principal diferença é que, enquanto as consultas são feitas apenas uma vez, as assinaturas são conectadas ao servidor e são atualizadas automaticamente quando há qualquer alteração nessa assinatura específica.

Primeiro temos que instalar:

npm install subscriptions-transport-ws
Enter fullscreen mode Exit fullscreen mode

Em seguida, vamos ao nosso index.js para importar e usá-lo.

 import { WebSocketLink } from "@apollo/client/link/ws";

//configurando nossos soquetes da web usando WebSocketLink
const link = new WebSocketLink({
  uri: `ws://localhost:4000/`,
  options: {
    reconnect: true,
  },
});
const client = new ApolloClient({
  link,
  uri: "http://localhost:4000",
  cache: new InMemoryCache(),
});
Enter fullscreen mode Exit fullscreen mode

Obs: uri no bloco de código diretamente acima é para nosso endpoint.

Em seguida, entramos em nosso componente e, em vez da consulta como fizemos acima, usaremos esta assinatura:

import {  useMutation, useSubscription } from "@apollo/client";
//iniciar nossa assinatura no lado do cliente

const ALL_PETS = gql`
  subscription AllPets {
    pets {
      id
      name
      type
      img
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

E ao invés de usar useQuery, acessaríamos nossos dados usando useSubscription.

 const getMessages = useSubscription(ALL_PETS);
Enter fullscreen mode Exit fullscreen mode

Optimistic UI

Optimistic UI é um pouco diferente no sentido de que não está sincronizada com o servidor, como uma assinatura. Quando fazemos uma mutation, ao invés de esperar por outro pedido do servidor, ele usa automaticamente os dados já inseridos para atuailizar a lista de pets imediatamente. Então, assim que os dados originais do servidor chegarem, eles substituirão a Optimistic UI. Isso também é diferente de "Atualizar o cache diretamente usando a função update no useMutation", embora ainda vamos atualizar o cache neste processo.

import React, { useState } from "react";
import gql from "graphql-tag";
import { useQuery, useMutation } from "@apollo/client";
import Loader from "./Loader";
import PetSection from "./PetSection";

//ALL_PETS usa gql de @apollo/client para nos permitir enviar consultas aninhadas
const ALL_PETS = gql`
  query AllPets {
    pets {
      id
      name
      type
      img
    }
  }
`;

//NEW_PET usa gql de @apollo/client para criar mutations
const NEW_PET = gql`
  mutation CreateAPet($newPet: NewPetInput!) {
    addPet(input: $newPet) {
      id
      name
      type
      img
    }
  }
`;

function OptimisticPets() {
//Usamos useQuery para lidar com a resposta ALL_PETS e atribuí-la a animais de estimação
  const pets = useQuery(ALL_PETS);
//Usamos useMutation para lidar com mutations e atualizar ALL_PETS.
  const [createPet, newPet] = useMutation(NEW_PET
    , {
    update(cache, {data: {addPet}}) {
      const allPets = cache.readQuery({query: ALL_PETS})
      cache.writeQuery({
        query: ALL_PETS,
        data: {pets: [addPet, ...allPets.pets]}
      })
    }
  });;
  const [name, setName] = useState("");
  const type = `DOG`;
 //Lida com a mutation e cria a resposta optimistic
  const onSubmit = (input) => {
    createPet({
      variables: { newPet: input },
      optimisticResponse: {
        __typename: 'Mutation',
        addPet: {
          __typename: 'Pet',
          id: Math.floor(Math.random() * 1000000) + '',
          type: "CAT",
          name: input.name,
          img: 'https://via.placeholder.com/300',
        }
      }
    });
  };

//Aqui está o nosso envio aciona a função onSubmit
  const submit = (e) => {
    e.preventDefault();
    onSubmit({ name, type });
  };
//retorna o carregamento do componente quando os dados ainda estão sendo carregados
  if (pets.loading ) {
    return <Loader />;
  }
//percorre os animais de estimação e os exibe no componente PetSection
  const petsList = pets.data.pets.map((pet) => (
    <div className="col-xs-12 col-md-4 col" key={pet.id}>
      <div className="box">
        <PetSection pet={pet} />
      </div>
    </div>
  ));
  return (
    <div>
      <form onSubmit={submit}>
        <input
          className="input"
          type="text"
          placeholder="pet name"
          value={name}
          onChange={(e) => setName(e.target.value)}
          required
        />
        <button type="submit" name="submit">
          add pet
        </button>
      </form>
      <div>
        {petsList}
      </div>

    </div>
  );
}
export default OptimisticPets;
Enter fullscreen mode Exit fullscreen mode

Quando o código acima chama onSubmit, o cache do Apollo Client armazena um objeto addPet com os valores dos campos especificados em optimisticResponse. No entanto, ele não substitui o cache principal pets(ALL_PETS) com o mesmo identificador de cache. Em vez disso, ele armazena uma versão separada e otmista do objeto. Isso garante que nossos dados em cache permaneçam precisos se o optimisticResponse estiverem errados.

O Apollo Client notifica todas as consultas ativas que incluem o arquivo pets(ALL_PETS). Essas consultas são atualizadas automaticamente e seus componentes associados são renderizados novamente para mostrar nossos dados otimistas. Isso não requer nenhuma solicitação de rede, portanto, é exibido instantaneamente para o usuário.

Eventualmente, nosso servidor responde ao real da mutação para obter o objeto addPet correto. Em seguida, o cache do Apollo Client descarta nossa versão otimista do objeto addPet. Ele também substitui a versão em cache pelos valores retornados do servidor.

O Apollo Client notifica imediatamente todas as consultas afetadas novamente. Os componentes em questão são renderizados novamente, mas se a resposta do servidor corresponder ao nosso optimisticResponse, todo o processo ficará invisível para o usuário.

Usando o Apollo como uma ferramenta de gerenciamento de estado no lado do cliente

Quando pensamos em ferramentas de gerenciamento de estado ou bibliotecas relacionadas ao react, o redux vem à mente. Curiosamente, o Apollo também pode atuar como uma ferramenta de gestão para o nosso estado local. Semelhante ao que temos feito com nossa API.

Schemas e Resolvers no lado do cliente

Para conseguir isso, teremos que escrever esquemas no lado do cliente para definir o tipo de dados que queremos e como queremos que sejam estruturados. Para isso, criaremos Client.js onde definiremos os schemas e resolvers, após o que o tornaremos globalmente acessível em nosso projeto com o cliente Apollo.

Para esse exemplo, estarei estendendo o tipo User que já existe, para adicionar o height como um número inteiro. Os resolvers também são adicionados para preencher o campo height em nosso schema.

import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { ApolloLink } from 'apollo-link'
import { HttpLink } from 'apollo-link-http'
import { setContext } from 'apollo-link-context'
import gql from 'graphql-tag'

//Estendendo o tipo User
const typeDefs = gql`
  extend type User {
    height: Int
  }
`

//Declarando nosso height dentro de nossos resolvedores no lado do cliente
const resolvers = {
  User : {
    height() {
      return 35
    }
  }
}
const cache = new InMemoryCache()
const http = new HttpLink({
  uri: 'http://localhost:4000/'
})
const link = ApolloLink.from([
  http
])

const client = new ApolloClient({
  link,
  cache,
  typeDefs,
  resolvers
})
export default client

client.js
Enter fullscreen mode Exit fullscreen mode

Podemos então importar o client para o nosso index.js:

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

//importing our client.js file into ApolloProvider
ReactDOM.render(
  <ApolloProvider client={client}>
    <Routing />
  </ApolloProvider>,
  document.getElementById("root")
);

index.js
Enter fullscreen mode Exit fullscreen mode

Dentro do componente, ele vai usar assim. Adicionamos @client para indicar que a consulta é do lado do cliente e não deve tentar puxá-la do servidor.

const ALL_PETS = gql`
  query AllPets {
    pets {
      id
      name
      type
      img
      owner {
        id
        height @client
      }
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

Portanto, estamos extraindo dados do servidor e do cliente na mesma consulta, e eles estarão acessíveis por meio do hook useQuery.

Criando Fragmentos e reutilizando consultas

Ás vezes, podemos precisar obter a mesma consulta em diferentes componentes. Portanto, em vez de codificá-la várias vezes, atribuímos essa consulta a algum tipo de variável e usamos essa variável.

Em nosso componente, apenas definimos o fragmento como PetFields sobre Pet (que é o tipo). Dessa forma, podemos apenas us´-ala em nossa query e mutation.

const DUPLICATE_FIELD = gql`
  fragment PetFields on Pet {
      id
      name
      type
      img
  }
`
const ALL_PETS = gql`
  query AllPets {
    pets {
      ...PetFields
    }
  }
  ${DUPLICATE_FIELD}
`;
const NEW_PET = gql`
  mutation CreateAPet($newPet: NewPetInput!) {
    addPet(input: $newPet) {
        ...PetFields
    }
  }
  ${DUPLICATE_FIELD}
`;
Enter fullscreen mode Exit fullscreen mode

Diretivas Apollo

Ao fazer consultas, podemos querer ter algumas condicionais que removam ou incluam um campo ou fragmento se uma determina condição for atendida ou não. As diretivas padrão incluem:

@skip: Indica que um campo/fragmento deve ser ignorado se uma condição for atendida.

const ALL_PETS = gql`
  query AllPets($name: Boolean!){
    pets {
      id
      name @skip: (if: $name)
      type
      img
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

Em $name está um booleano que é adicionado como uma variável quando estamos chamando essa consulta. Está sendo utilizando com @skip para determinar quando exibir o campo name. Se verdadeiro, ele pula, se for false, resolve esse campo.

@includes também funcionam de maneira semelhante. Se a condição for true, esse campo é resolvido e adicionando, e se for false, não é resolvido.

Temos também @deprecated que pode ser usado cpm schemas para retirar campos, onde você pode até adicionar motivos.

Também temos bibliotecas que nos permitem adicionar ainda mais diretivas, elas podem ser úteis ao criar coisas um tanto complicadas com o GraphQL.

Dicas e truqyes com o uso do GraphQL Lodash dentro de suas consultas

GraphQL Lodash é uma biblioteca que pode nos auxiliar em uma consulta de forma mais eficiente, mais como uma forma avançada das diretivas Apollo.

Ele pode ajudá-lo a consultar seu servidor de uma forma que retorne dados de forma mais organizada e compacta. Por exemplo, você está consultando title de films assim:

films {
  title
}
Enter fullscreen mode Exit fullscreen mode

E retorna os títulos de filmes como objetos em uma array:

"films": [
    {
      "title" : "Prremier English"
    },
    {
      "title" : "There was a country"
    },
    {
      "title" : "Fast and Furious"
    }
    {
      "title" : "Beauty and the beast"
    }
]
Enter fullscreen mode Exit fullscreen mode

Mas, quando usamos a diretiva map de lodash, podemos percorrer a array de filmes para ter uma única array com todos os títulos como filhos diretos.

"films": [  
  "Premier English",
  "There was a country",
  "Fast and Furious",
  "Beauty and the beast"
]
Enter fullscreen mode Exit fullscreen mode

Outra que se mostra útil, é a diretiva keyby. Você pode enviar uma consulta simples como essa:

people {
  name
  age
  gender
}
Enter fullscreen mode Exit fullscreen mode

Resposta:

"people" : [
  {
    "name":  "James Walker",
    "age": "19",
    "gender": "male"
  },
  {
    "name":  "Alexa Walker",
    "age": "19",
    "gender": "female"
  }, 
]
Enter fullscreen mode Exit fullscreen mode

Vamos usar a diretiva @_keyup em nossa consulta:

people @_(keyBy: "name") {
  name
  age
  gender
}
Enter fullscreen mode Exit fullscreen mode

A resposta ficará assim:

"people" : [
  "James Walker" : {
     "name":  "James Walker",
     "age": "19",
     "gender": "male"    
  }
  "Alexa Walker" : {
     "name":  "Alexa Walker",
     "age": "19",
     "gender": "female"
  }
]
Enter fullscreen mode Exit fullscreen mode

Portanto, neste caso, cada resposta tem uma chave, que é a name da pessoa.

Conclusão

Neste artigo, abordamos tópicos avançados para obter atualização de dados em tempo real usando a função update(), assinatura e Optimistic UI. Tudo em um pouco para melhorar a experiência do usuário.

Também abordamos o uso do GraphQL para gerenciar o estado no lado do cliente e a criação de consultas reutilizáveis ​​com fragmentos do GrahQL. Este último nos permite usar as mesmas consultas em diferentes componentes onde for necessário, sem ter que repetir tudo todas as vezes.

No final, passamos pelas diretivas Apollo e Grahql Lodash para nos ajudar a consultar nossos servidores de maneira mais rápida e melhor.

Top comments (0)