Cómo generar automáticamente Queries y Mutations de GraphQL con tipos estáticos en un proyecto de React

GraphQL tiene mucho que ofrecer y puede simplificar considerablemente la vida de un desarrollador Front-End. Además de ser la mejor manera de obtener datos (en mi opinión), es un schema de tipo estático que no solo podemos usar para generar tipos automáticamente, sino que también podemos usarlo para generar código automáticamente.

Soy un gran fanático del uso de GraphQL y TypeScript, en cualquier proyecto de React que necesite interactuar con un schema GraphQL. La cantidad de tiempo que podemos ahorrar en desarrollo y la gran experiencia de desarrollo que ofrece hace que sea obvio (al menos para mí).

He estado usando Apollo Client en proyectos de React durante algún tiempo. En los viejos tiempos (antes de Hooks) se podía usar HoC o los componentes Query y Mutation para interactuar con el schema de GraphQL. Esta era una forma muy declarativa y simple de comenzar, pero no era escalable y añadir tipos estáticos no era nada agradable. Al final, esta no era la mejor manera de consumir un schema de GraphQL en un proyecto de React desde ningún punto de vista. En este momento están deprecados y solo se puede usar Hooks, que es mucho más simple, escalable y fácil de usar, como verán.

La forma manual

Apollo Client incluye muchas funcionalidades, pero por ahora nos centraremos solo en los hooks para efectuar queries y mutations. Consulte su guía de inicio si desean aprender cómo configurar un cliente en su proyecto.

Queries

Para ejecutar cualquier Query necesitamos usar el hook useQuery. Es bastante simple, se tiene que pasar el documento de GraphQL y también se puede pasar  valores de configuración como las variables del Query.

Por ejemplo, si queremos consultar el tipo User, tenemos que seguir estos pasos:

  1. Cree nuestro documento de GraphQL

  2. Llame al hook useQuery con nuestro documento y parámetros de configuración

  3. Intentar recordar los valores que están disponibles para el resultado del query y sus variables (todavía no hemos introducido TS)

Echemos un vistazo a este ejemplo de código:

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

// Documento de GraphQL 
const UserQuery = gql`
    query ($id: ID!){
      User(id: $id) {
        id
        avatar
        fullName
      }
    }
`;

const User = () => {
	// Llama al Hook useQuery con nuestro documento y config 
  const {data} = useQuery(UserQuery, { variables: { id: 1 } })
  if (!data || !data.User) return null;
  return (
    <section>
      <h1>User</h1>
      <div>
        <div>
          <img src={data.User.avatar} alt={`${data.User.fullName} avatar`} />
          <p>{data.User.fullName}</p>
        </div>
      </div>
    </section>
  );
};

export default User;

Para un Query muy simple, esto parece correcto, pero tendríamos que repetir el mismo proceso cada vez que quisiéramos utilizar esta Query. A largo plazo, esto no es ideal. Probablemente algunas personas opinan que podríamos usar Fragments o crear nuestros propios hooks, y probablemente eso ayudaría mucho. Sin embargo, lo que realmente no me gusta de esto es que necesitaría recordar la respuesta del Query y para queries con tipos anidados pasaremos mucho tiempo mirando el esquema repetidamente.

Mutations

Podemos llamar a nuestras Mutations de la misma forma sencilla que a nuestras queries. Necesitamos usar el hook useMutation, pasar el documento GraphQL y las variables y eso es todo. Luego, podemos usar el método mutate para ejecutar nuestra mutación y el objeto data que representa el estado actual de la mutación.

Por ejemplo, si queremos eliminar un usuario, debemos seguir estos pasos:

  1. Cree nuestro documento GraphQL

  2. Cree una instancia del hook useMutation con nuestro documento

  3. Ejecuta la mutación (en algún momento)

  4. Intentar recordar los valores que están disponibles para el resultado de la mutación y sus variables

Echemos un vistazo a este ejemplo de código:

import React, {useEffect} from 'react';
import {gql} from "apollo-boost";
import {useMutation} from "@apollo/react-hooks";

// Documento de GraphQL 
const deleteUserMutation = gql`
  mutation deleteUser($id: ID!) {
    deleteUser(id: $id) {
      id
    }
  }
`;

const DeleteUser = () => {
	// Crea una instancia del Hook useMutation con nuestro documento 
  const [deleteUser, { data }] = useMutation(deleteUserMutation);

  const onDeleteUserClick = () => (
		// Ejecuta la mutación 
    deleteUser({ variables: { id: 1 } })
  );

  useEffect(() => {
    // Hacer algo cuando cambien los datos de deleteUser 
  }, [data])

  return (
    <div>
      <button onClick={onDeleteUserClick}>
        Delete User
      </button>
    </div>
  );
};

export default DeleteUser;

Si bien esto es simple y muy sencillo, tiene los mismos problemas: reusabilidad y la falta de tipos estáticos.

Añadiendo TypeScript

Apollo Client es muy compatible con TypeScript. Tanto para los Hooks de Query como los de Mutation se pueden añadir tipos estáticos relativamente fácil.

Queries

Podemos agregar tipos (más o menos) al ejemplo de Query anterior con solo tres pasos adicionales:

  1. Crear una nueva interfaz para los datos del Query

  2. Cree una nueva interfaz para las variables del Query

  3. Utilizar esas interfaces en el Hook useQuery

Echemos un vistazo al ejemplo:

import React from 'react';
import {gql} from "apollo-boost";
import {useQuery} from "@apollo/react-hooks";
import styled from 'styled-components';

// ------- Nuevo 💡 ------- 
// Interfaz de datos de consulta GraphQL
interface IUserQueryData {
  User: {
    avatar: string;
    fullName: string;
  };
}

// ------- Nuevo 💡 ------- 
// Interfaz de variables de consulta GraphQL
interface IUserQueryVariables {
  id: string | number;
}

// GraphQL Document
const UserQuery = gql`
    query ($id: ID!){
      User(id: $id) {
        id
        avatar
        fullName
      }
    }
`;

const User = () => {
// ------- Nuevo 💡 ------- 
  const {data} = useQuery<IUserQueryData, IUserQueryVariables>(UserQuery, { variables: { id: 1 } })
  if (!data || !data.User) return null;
  return (
    <section>
      <h1>User</h1>
      <div>
        <div>
          <img src={data.User.avatar} alt={`${data.User.fullName} avatar`} />
          <p>{data.User.fullName}</p>
        </div>
      </div>
    </section>
  );
};

export default User;

Con un par de líneas nuevas, puedes tener un Hook useQuery parcialmente tipado. Si bien esto funciona, no es tan fácil de mantener. Para cualquier cambio que se realice al Query, se tiene actualizar las interfaces. Además, el error humano podría provocar una mala experiencia de desarrollo y algunos pasos adicionales innecesarios.

Mutations

Necesitamos hacer los mismos pasos adicionales para agregar tipos a nuestro hook useMutation:

import React, {useEffect} from 'react';
import {gql} from "apollo-boost";
import {useMutation} from "@apollo/react-hooks";

// ------- Nuevo 💡 ------- 
// Interfaz de datos de consulta GraphQL
interface IUserMutationData {
  User: {
    id: string;
  };
}

// ------- Nuevo 💡 ------- 
// Interfaz de variables de consulta GraphQL
interface IUserMutationVariables {
  id: string | number;
}

// GraphQL Document
const deleteUserMutation = gql`
  mutation deleteUser($id: ID!) {
    deleteUser(id: $id) {
      id
    }
  }
`;

const DeleteUser = () => {
   // ------- Nuevo 💡 ------- 
  // Crea una instancia del Hook useMutation con nuestro documento y tipos
  const [deleteUser, { data }] = useMutation<IUserMutationData, IUserMutationVariables>(deleteUserMutation);

  const onDeleteUserClick = () => (
  	// Ejecuta la mutación 
    deleteUser({ variables: { id: 1 } })
  );

  useEffect(() => {
    // Hacer algo cuando cambien los datos de deleteUser 
  }, [data])

  return (
    <div>
      <button onClick={onDeleteUserClick}>
        Delete User
      </button>
    </div>
  );
};

export default DeleteUser;

Una vez que tenemos un Query y una Mutation, está claro que hay algunos problemas si intentamos escalar esto manualmente.

GraphQL Code Generator

Identifiqué dos problemas comunes al usar Apollo Client + React + TypeScript. Si bien los hooks son muy simples y rápidos de usar, no estamos aprovechando al máximo las ventajas de GraphQL, ya que podemos generar automáticamente tipos y código 

Hay un increíble proyecto de código abierto llamado GraphQL Code Generator. Esta es una herramienta CLI que puede automatizar tanto la generación de tipos como de código. Solo se tiene que crear los documentos GraphQL en algún lugar de tu código base y paquete hará el resto.

Instacion

Tienes que instalar graphql  (probablemente ya lo tengas instalado) y @graphql-codegen/cli:

# Instalar GraphQL
yarn add graphql

#Instalar la herramienta CLI GraphQL Codegen 
yarn add -D @graphql-codegen/cli

Puedes usar yarn o npm según tus preferencias, pero para evitar conflictos de GraphQL, evita instalar estos paquetes globalmente.

Configuración

GraphQL Code Generator viene con un asistente de configuración que te ayuda a crear un archivo de configuración inicial; después de instalar el paquete, debes ejecutar este comando:

yarn graphql-codegen init

Tienes que responder algunas preguntas sobre tu proyecto y seleccionar las funciones que desea utilizar. No se tiene ejecutar este comando en todos los proyectos ya que se puede escribir la configuración en un archivo codegen.yml o codegen.json. Mira la referencia de la configuración completa.

Este es mi codegen.yml:

overwrite: true
generates:
  src/generated/graphql.tsx:
    schema: "<http://localhost:5000/graphql>"
    documents: "src/graphql/**/*.ts"
    plugins:
      - add: // tslint:disable
      - "typescript"
      - "typescript-operations"
      - "typescript-react-apollo"
    config:
      withHooks: true

Todos mis documentos de GraphQL están dentro del directorio src / graphql / de esta manera podemos decirle a la herramienta codegen que solo busque documentos en esa carpeta.

Plugins

GraphQL Code Generator usa plugins para extender y configurar su funcionalidad, puedes agregar solo los que va a usar, lo cual es genial. Para nuestros propósitos, tenemos que instalar los siguientes:

yarn add -D @graphql-codegen/add @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo

Consulta la lista de plugins completa.

Correr el comando

Puedes llamar a la herramienta directamente, pero para que sea más fácil, podemos agregar un nuevo script en el package.json del proyecto:

{
  "scripts": {
		"generate-graphql-code": "graphql-codegen --config codegen.yml"
  }
}

Entonces podemos llamarlo desde la terminal de la siguiente manera:

yarn generate-graphql-code

Generación Automática Del Código

Estructura del proyecto 

Quiero usar GraphQL Code Generator para simplificar el flujo de trabajo. No sería ideal tener un solo archivo con todas las queries y mutations, así que opté por crear una nueva carpeta para cada tipo de GraphQL que usaría en el proyecto. Dentro de cada carpeta, tenemos dos archivos, queries.ts y mutations.ts cada uno con sus correspondientes operaciones de GraphQL. Con esta estructura de carpetas es extremadamente fácil saber dónde editar o agregar nuevas operaciones.

Si aplicamos esto al tipo User, se vería así:

.
├── src
	├── components
	├── graphql //This is where our GraphQL Documents will live
		├── User
			── queries.ts
			── mutations.ts
	├── generated
		── graphql.tsx // This is the code generator output
├── codegen.yml // This is the code generator config file
──tsconfig.json
──package.json

Queries

Esta vez necesitamos crear el documento GraphQL en el archivo queries.ts dentro ./src/graphql/User. Tenemos que nombrar nuestra Query y exportarla para que el generador de código pueda crear los tipos y el Hook por nosotros.

import { gql } from 'apollo-boost';

export const userQuery = gql`
  query User($id: ID!) {
    User(id:$id) {
      id
      fullName
      avatar
    }
  }
`;

Después de haber agregado nuestra Query, necesitamos ejecutar el generador de código y estaremos listos para usar nuestro Hook. De forma predeterminada, los Hooks se crearán con esta nomenclatura: use [nombre de la Query]Query. Para nuestro ejemplo, el nombre del Hook sería useUserQuery. Simplemente podemos importarlo y hemos terminado. No olvides que este Hook está completamente tipado 😉.

import React from 'react';
// Importar nuestro Hook autogenerado
import { useUsersQuery } from '../generated/graphql';

const Users:React.FC = () => {
  // Usar nuestro Hook autogenerado
  const { data } = useUserQuery();
  if (!data || !data.User) return null;
  return (
    <section>
      <h1>User</h1>
      <div>
        <div>
          <img src={data.User.avatar} alt={`${data.User.fullName} avatar`} />
          <p>{data.User.fullName}</p>
        </div>
      </div>
    </section>
  );
};

export default User

Mutations

Para la mutación también tenemos que crear primero el documento de GraphQL, exportarlo y no olvidar ponerle un nombre. Los Hooks de mutación generados automáticamente utilizan esta nomenclatura: use [nombre de la Mutation]Mutation.

import { gql } from 'apollo-boost';

export const deleteUserQuery = gql`
  mutation deleteUser($id: ID!) {
    deleteUser(id: $id) {
      id
    }
  }
`;

Después de generar nuestro código, podemos importar y usar nuestro Hook de mutation completamente tipado:

import React, {useEffect} from 'react';
// Importar nuestro Hook autogenerado
import {useDeleteUserMutation} from '../generated/graphql';

const DeleteUser = () => {
  // Usar nuestro Hook autogenerado
  const [deleteUser, { data }] = useDeleteUserMutation;

  const onDeleteUserClick = () => (
    deleteUser({ variables: { id: 1 } })
  );

  useEffect(() => {
    // Hacer algo cuando cambien los datos de deleteUser 
  }, [data])

  return (
    <div>
      <button onClick={onDeleteUserClick}>
        Delete User
      </button>
    </div>
  );
};

export default DeleteUser;

Concluyendo

Implementar GraphQL y TypeScript en un proyecto de React podría ser una tarea muy larga o algo bastante simple. Desde mi punto de vista, no importa si solo se planea usar un par de queries y mutations o una gran cantidad de ellas, no vale la pena crear sus documentos de GraphQL y tipos a mano cuando se puede automatizar todo el proceso. y centrarte sólo en lo que realmente importa.

Enlaces útiles

https://graphql-code-generator.com/

https://www.apollographql.com/docs/react/