apollo logo

Apollo Client v3

¡Hola #Yobers!

Hoy os traemos nuevo artículo para que el confinamiento en casa os sea mucho más ameno. En él os hablamos sobre qué es Apollo Client, qué mejoras tiene la versión 3, cómo se instala y cómo se debe configurar y modificar la caché. Esperamos que os guste y entretenga en estos días tan difíciles que estamos viviendo.

Lo primero de todo es entender qué es Apollo Cliente. Si tienes una aplicación que tome los datos de una API GraphQL, seguramente hayas oído hablar de Apollo, que además de ayudarte con las peticiones, se encarga de mantener en caché los datos devueltos para evitar sobrecargar la red. Tiene clientes para para integrar Apollo con varias plataformas: React, Angular, Vue, iOS o Android, entre otros.

Una vez, entendemos qué es Apollo Client debemos saber qué mejoras ha traído con su nueva versión. Bien es cierto que la versión 2.6 supuso un cambio enorme con la integración de los hooks, que mejora enormemente la legibilidad del código, la gestión/actualización de la caché seguía siendo un proceso tedioso, algo que viene a solucionar la versión 3.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Con react-components
const MiComponente = () => {
  return (
    <Query query={MI_QUERY}>
      {({ loading, error, data }) => {
        if (loading) return <p>"Loading..."</p>;
        if (error) return <p>Error fetching: {error.message}</p>;
        return <MiDataContainer data={data} />;
      }}
    </Query>
  );
};
// Con react-hooks (v2.6)
const MiComponente = () => {
  const { loading, error, data } = useQuery(MI_QUERY);
  if (loading) return <p>"Loading..."</p>;
  if (error) return <p>Error fetching: {error.message}</p>;
  return <MiDataContainer data={data} />;
};

Pero no solo se ha solucionado la gestión/actualización de la caché en esta última versión, también, se ha simplificado la instalación. Ahora se han unificado todos los paquetes en uno único. De tal manera que los paquetes apollo-client apollo-cache-inmemory apollo-link-http @apollo/react-hooks graphql  pasan a ser accesibles desde @apollo/client .

Instalación

Pero… ¿Cómo lo instalamos? ¡Muy sencillo #yobers!

Creamos el cliente de Apollo y lo conectamos a nustra App mediante el provider:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from "react";
import { render } from "react-dom";
import {
  ApolloClient,
  ApolloProvider,
  HttpLink,
  InMemoryCache
} from "@apollo/client";

const client = new ApolloClient({
  cache: new InMemoryCache(),
  link: new HttpLink({
    uri: "https://miservidor.com"
  })
});
const App = () => (
  <ApolloProvider client={client}>
    <FullAppConAccesoAlClient />
  </ApolloProvider>
);

render(<App />, document.getElementById("root"));

Ahora ya podemos hacer las llamadas a useQuery, useMutation… dentro de nuestros componentes.

Configurar la caché

Una vez se ha instalado, debemos configurar la caché. El constructor de la caché en la inicialización del cliente acepta varios parámetros para configurar su comportamiento. Podemos configurar una TypePolicy para un tipo de datos al que queramos, por ejemplo, definir un campo identificador diferente al de por defecto (id o _id), especificando keyFields.

Si definimos este campo como false, deshabilitamos la normalización de ese tipo, y solo será accesible desde su tipo padre. Desde las typePolicies, también, podemos configurar el comportamiento de lectura y escritura en la caché de cada campo de un tipo, con read y merge, respectivamente. Así podemos definir, que cuando se haga una petición de más comentarios de un usuario, se añadan a los ya existentes y no sobrescriba.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const cache = new InMemoryCache({
  typePolicies: {
    Usuario: {
      keyFields: ["dni"],
      fields: {
        comentarios: {
          merge: (existing: User[] = [], incoming: User[] = []): User[] => [
            ...existing,
            ...incoming
          ]
        }
      }
    },
    Comentario: {
      fields: {
        fecha: {
          read: (value: string) => new Date(value)
        }
      }
    }
  }
});

Esto se puede utilizar para un caso muy común, como, por ejemplo: la paginación. Ya que la función read también recibe en un segundo parámetro los argumentos de la petición, y, además, reutilizando la lógica.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function paginationFieldPolicy<T>() {
  return {
    merge(existing: T[] = [], incoming: T[] = [], { args, readField }) {
      const merged = existing ? existing.slice(0) : [];
      // Insertar los nuevos elementos en su lugar adecuado
      // Dado por los argumentos offset y limit
      const end = args.offset + Math.min(args.limit, incoming.length);
      for (let i = args.offset; i < end; ++i) {
        merged[i] = incoming[i - args.offset];
      }
      return merged;
    },
    read(existing: T[], { args }) {
      // En la primera lectura, antes de haber introducido datos en la
      // cache, se devolverá undefined, que indica que no existe el campo
      const page =
        existing && existing.slice(args.offset, args.offset + args.limit);
      // Comprobamos que existen datos entre los límites indicados
      // en los argumentos
      if (page && page.length > 0) {
        return page;
      }
    }
  };
}

const cache = new InMemoryCache({
  typePolicies: {
    Usuario: {
      fields: {
        publicaciones: paginationFieldPolicy()
      }
    }
  }
});

Modificar la caché

Pero, no solo puedes configurar cómo se comportará la caché en lectura y escritura, también puedes modificarla. Y ahí es donde entra la v3. Ahora tenemos varias funciones nuevas que nos pueden hacer la vida un poco más fácil: modify , evict y gc .

Con la primera, modify, podemos modificar el valor de la caché directamente, digamos que es una mezcla de las dos funciones que ya existían en la librería: readQuery y writeQuery. Con ello podemos hacer los cambios tras las mutaciones de la siguiente manera:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
updateComment({
  variables: {
    id,
    message: newMessage
  },
  update(cache, { data: updateComment }) {
    if (updateComment.ok) {
      // Actualizar el campo message del comentario
      cache.modify(`Comentario:${id}`, {
        message(previousValue: string) {
          return newMessage;
        }
      });
    }
  }
});
// Eliminar un comentario
cache.modify("ROOT_QUERY", {
  comentarios(comentarios: Reference[], { toReference }) {
    const ref = toReference({
      __typename: "Comentario",
      id
    });
    return comentarios.filter(c => c.__ref !== ref.__ref);
  }
});


Evict elimina un objeto normalizado de la cache a partir de su identificador. Una vez borrado, puede ser que otros elementos tuviesen referencia a ese objeto. Para ello está la función gc (garbage collector), que se encarga de eliminar todas esas referencias inconexas.

Evict parece lo más sencillo para eliminar objetos normalizados, sin embargo, parece que no actualiza la UI, solo modifica la caché.

1
2
cache.evict('Comentario:12345678') // Eliminará el comentario con id 12345678
cache.gc() 

Y… ¡Eso es todo #yobers!

Con este artículo serás capaz de sacar partido a Apollo Client en su nueva versión 3.0. Te hemos enseñado qué es, qué mejoras tiene, cómo instalarlo y cómo configurar y modificar la caché. Pero, si no sabes realizar alguno de los pasos que os hemos indicado ¡no pasa nada! Ponte en contacto con nosotros y nosotros nos encargamos de todo.

¡Hasta la próxima semana!