Configurar Apollo Client en React, usando Hooks

August 22, 2019 • ☕️ 6 minutos de lectura

Introducción

Aprenderemos a conectar graphql con nuestra aplicación React, para ello usaremos hooks y apollo boost.

Vamos a crear una app en react que se conecta a una API Graphql. Para este ejemplo nos conectaremos a la API Rick and Morty.

npx cerate-react-app rickandmory-graphql-react
cd rickandmory-graphql-react/
yarn start

Dependencias

apollo boost: Es un kit de inicio que contiene todo lo que necesitamos para configurar ApolloClient.

@apollo/react-hooks: Reactiva la integración de la capa de vista basada en ganchos graphql

graphql Analiza las consultas GraphQL.

npm i apollo-boost graphql react-apollo-hooks

Crear cliente y conectarlo a React

Ahora que tenemos instaladas las dependencias, crearemos nuestro ApolloClient, solo necesitamos la uri a la que realizaremos las consultas, en nuestro caso https://rickandmortyapi.com/graphql/.

Después conectaremos React con ApolloProvider, un componente exportado desde @apollo/react-hooks.

ApolloProvider es similar a Context.Provider. Envuelve toda la aplicación y nos permite acceder al cliente desde cualquier parte del árbol de componentes.

⚠️ obviaremos la configuración de @reach/routerpara centrarnos solo en lo importante.

✏️index.js

import React from 'react';
import ReactDOM from 'react-dom';
import ApolloClient from 'apollo-boost'
import { ApolloProvider } from '@apollo/react-hooks';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { Router } from "@reach/router";

const client = new ApolloClient ({
	uri: 'https://rickandmortyapi.com/graphql/' 
})

ReactDOM.render(
<ApolloProvider client={client}>
  <Router>
    <App path="/"/>
  </Router>
</ApolloProvider>, document.getElementById('root'));
serviceWorker.unregister();

Consulta simple

Ahora que tenemos el cliente creado y ApolloProvider conectado, podemos empezar a solicitar datos usando el hook useQuery. Para ello debemos definir la query usando gql y pasarlo como parámetro a useQuery. Esto nos devolverá un objeto con las propiedades {loading, data, error }.

  • loading: Estará en false hasta que devuelva el resultado de nuestra consulta, que pasará a true. Esto es ideal para mostrar un spinner o skeleton screen mientras esperamos el resultado, así mejorando la experiencia de usuario, mostrándole algo mientras se cargan los resultados.
  • error: Contendrá los errores en el caso de que existan.
  • data: Contendrá los datos de nuestra consulta.

Es hora de poner esto en acción, ya que estamos usando la api de Rick and Morty, mostraremos un listado de los personajes, para después al pulsar sobre cada uno de ellos nos lleve a su ficha con sus detalles.

¡Vamos a ello!

App.js

import React from 'react'
import { gql } from 'apollo-boost'
import { useQuery } from '@apollo/react-hooks'
import { Link } from "@reach/router";
const getCharacters = gql`
query getCharacters {
  characters {
    results {
      id
      name
      status
      image
    }
  }
}
`

export const ListOfCharacters = () => {
  const { loading, data, error } = useQuery(getCharacters)
  if (loading) return 'Cargando'
  if (error) return `Error: ${error}`
  return (
    <div className="App">
      {data.characters.results.map(post => (
        <Link to={`character/${post.id}`}>
        <div className="card" key={post.id}>
          <div className="content">
            <img src={post.image} alt={`Imagen de ${data.character.name}`}/>
            <h1>{post.name}</h1>
            <p>{post.status}</p>
          </div>
        </div>
        </Link>
      ))}
    </div>
  )
}

Enhorabuena 🎉🎉, ya hemos creado nuestro componente usando useQuery, ahora para probarlo solo debemos importarlo a nuestro app.js

Consultas pasando un parámetro

Ahora crearemos otro componente muy parecido, pero este solo cargará la información del personaje, para ello debemos pasarle su ID como parámetro.

Character.js

import React from 'react'
import { gql } from 'apollo-boost'
import { useQuery } from '@apollo/react-hooks'

const getCharacter = gql`
  query getCharacter($id: ID) {
    character(id: $id) {
      id
      name
      status
      species
      image
    }
  }
`

export const SingleCharacter = character => {
  const { loading, data, error } = useQuery(getCharacter, { variables: { id: character.id}} )
  if (loading) return ''
  if (error) return `Error: ${error}`
  return (
    <div className='App'>
        <div className="card">
          <div className="content">
            <img src={data.character.image} alt={`Imagen de ${data.character.name}`}/>
            <h1>{data.character.name}</h1>
            <p>{data.character.status}</p>
          </div>
        </div>
    </div>
  )
}

Para poder ver este nuevo componente en acción, vamos a añadirlo a @reach/router

import React from 'react';
import ReactDOM from 'react-dom';
import ApolloClient from 'apollo-boost'
import { ApolloProvider } from '@apollo/react-hooks';
import './index.css';
import App from './App';
import Character from './Character';import * as serviceWorker from './serviceWorker';
import { Router } from "@reach/router";

const client = new ApolloClient ({
	uri: 'https://rickandmortyapi.com/graphql/' 
})

ReactDOM.render(
<ApolloProvider client={client}>
  <Router>
    <App path="/"/>
    <Character path="character/:id"/>  </Router>
</ApolloProvider>, document.getElementById('root'));
serviceWorker.unregister();

Autentificación con tokens

Un de las formas más comunes de autentificación es usar tokens, en nuestro caso json web tokens (jwt), para ello debemos implementar jwt en nuestro servidor graphql.

Este nos devolverá un token, algo similar a este.

{
  token: 'eyJzdWIiOnsiX2lkIjoiNWQ1ODc4MjE5MDM4NDQ1NGZkY2E1ZjFiIiwiZW1haWwiOiJlbm1hc2thQGdtYWlsLmNvbSJ9LCJpYXQiOjE1NjYwOTgzNTEsImV4cCI6MTU2NzMwNzk1MX0'
}

Ahora deberíamos almacenarlo en en localStorage, para poder acceder a el en cualquier momento.

En este ejemplo, extraeremos el token de sesión de localStorage cada vez que se envíe una solicitud.

const client = new ApolloClient ({
  uri: 'http://localhost:4000',
  request: async operation => {
    const token = localStorage.getItem('token');
    operation.setContext({
      headers: {
        authorization: token ? `Bearer ${token}` : ''
      }
    });
  }
})

Extra Skelton Screen

Para mejorar la experiencia de usuario, crearemos un nuevo componente llamado skeleton.js, que no es más que unos divs, simulando el contenido que será cargado.

skeleton.js

import React from 'react'
import './Skeleton.css';
export default function skeleton() {
  return (
    <React.Fragment>
      <div className="card">
        <div className="content">
          <div className="skeleton-image"></div>
          <div className="line"></div>
          <div className="line"></div>
        </div>
      </div>
      <div className="card">
        <div className="content">
          <div className="skeleton-image"></div>
          <div className="line"></div>
          <div className="line"></div>
        </div>
      </div>
      <div className="card">
        <div className="content">
          <div className="skeleton-image"></div>
          <div className="line"></div>
          <div className="line"></div>
        </div>
      </div>
      </React.Fragment>
    )
}

skeleton.css

.card {
  width: 400px;
  margin-bottom: 1rem;
  box-shadow: 0 0 6.4px 4px rgba(0,0,0,.06275);
  border-radius: 4px;
  background-color: #fff;
  grid-template-columns: 200px 200px minmax(200px,auto);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  margin: 10px;
  padding: 10px;
}
.card img {
  min-width: 300px;
  min-height: 300px;
}
.line {
  height: 12px;
  margin: 10px;
  animation: pulse 1s infinite ease-in-out;
  border-radius: 5px;
}
.skeleton-image {
  height: 300px;
  margin: 10px;
  animation: pulse 1s infinite ease-in-out;
  border-radius: 5px;
}
.card .content div:nth-child(1){
  width: 300px;
}
.card .content div:nth-child(2){
  width: 300px;
}
.card .content div:nth-child(3){
  width: 270px;
}
@keyframes pulse {
  0% {
    background-color: rgba(165,165,165,.1)
  }
  50% {
    background-color: rgba(165,165,165,.3)
  }
  100% {
    background-color: rgba(165,165,165,.1)
  }
}

Ahora cambiado el if de loading y devolveremos el nuevo componente que creamos.

if (loading) return '<Skeleton/>'