DEV Community

Ricardo
Ricardo

Posted on • Updated on

Colocated Fragments: Como organizar tus queries en React

¿A quien va dirigido éste artículo?

Desarrolladores trabajando en un proyecto con React que consume datos de una API GraphQL y que desean conocer una alternativa para organizar la definición de sus queries.

Introducción

Existen múltiples formas para organizar queries en un proyecto con React, pero normalmente te encontrarás con alguna variación de los siguientes dos métodos:

  • Guardar todos los queries en un archivo único o centralizado. Por ejemplo, queries.ts.
  • Colocar las definiciones completas de los queries junto al componente padre que los consume. Ejemplo.

A lo largo de este artículo nos enfocaremos en aprender una variación del segundo método en el cual colocamos los queries junto a los componentes padre que los ejecutan, y a su vez, a través de Fragments colocamos cada campo consumido junto a los componentes hijos que los consumen.

¿Qué es un Fragment?

Un Fragment no es más que una unidad reutilizable de información.

De la documentación oficial de GraphQL:

Los Fragments te permiten construir un conjunto de campos para después incluirlos en los queries donde se necesiten.

¿Por qué son útiles los Fragments?

Usemos un proyecto de un Blog como ejemplo. Supongamos que tenemos un query de GraphQL post que se encarga de regresar una publicación de un blog incluyendo la información sobre su autor así como cada uno de los comentarios realizados en dicha publicación:

// Sin Fragment
post(id: ID!) {
  id
  title
  content
  date
  author {
    id
    name
    image
    email
  }
  comments {
    id
    content
    date
    author {
      id
      name
      image
      email
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Seguro notaste que solicitamos dos veces la misma información sobre usuarios (id, name, image, email). La primera sobre el autor del post y la segunda sobre los autores de los comentarios. Ahora veamos el mismo ejemplo, pero ahora utilizando Fragments:

// Con Fragment
post(id: ID!) {
  id
  title
  content
  date
  author {
    ...Avatar
  }
  comments {
    id
    content
    date
    author {
      ...Avatar
    }
  }
}

fragment Avatar on User {
  id
  name
  image
  email
}
Enter fullscreen mode Exit fullscreen mode

Como puedes ver, nombramos a nuestro Fragment Avatar e indicamos que sólo podrá ser utilizado por campos del tipo User. La manera de consumir Fragments es mediante el operador de GraphQL spread seguido del nombre del Fragment: ...Avatar. Todos los campos definidos en él serán incluidos en la sección donde se mande a llamar.

Los Fragments por sí solos son útiles, pero al combinarlos con los componentes de React, se vuelven todavía más potentes.

Colocated Fragments

De la documentación del cliente GraphQL Apollo:

Un Colocated Fragment es igual a cualquier otro Fragment, excepto que está adjunto a un componente en particular de React, el cual utiliza los campos de dicho Fragment

Básicamente, es "colocar" la creación del Fragment junto al componente que va a consumir su información.

Creando un Colocated Fragment

Utilicemos como ejemplo nuestro componente Avatar el cual se encarga de mostrar la información de un usuario.

Así quedaría nuestro Colocated Fragment:

// Avatar.jsx
import gql from 'graphql-tag';

export const Avatar = ({ user }) => {
  return (
    <div>
      <a href={`/user/${user.id}`}>
        <h3>{user.name}</h3>
        <img src={user.image} />
      </a>
    </div>
  );
};

Avatar.fragments = {
  user: gql`
    fragment Avatar on User {
      id
      name
      image
    }
  `
};
Enter fullscreen mode Exit fullscreen mode

En este ejemplo suceden 3 cosas importantes:

  • Primero definimos un nuevo Fragment llamado Avatar. No existe alguna regla explícita para nombrarlos, pero para evitar colisiones, una buena alternativa o sugerencia es llamarlos con el mismo nombre del componente al que están enlazados.
  • Después exportamos el Colocated Fragment mediante la creación de una nueva propiedad en el componente de React Avatar llamada fragments.
    • Esta forma de exportarlos viene de la propuesta en la documentación de Apollo, sin embargo, esto es cuestión de gustos, solo asegúrate de tener una convención que se respete en todo el proyecto (Si utilizas typescript, puedes crear un nuevo tipo para forzar que tus componentes incluyan el atributo fragments).
  • Por último, el componente consume la información mediante la prop user, la cual incluye los mismos campos que el Fragment: id, image y name . (si utilizas typescript, al final dejo un "paso por paso" sobre cómo generar un type automáticamente para tu prop basado en el Colocated Fragment).

Consumiendo un Colocated Fragment

La magia de los Colocated Fragments la encontramos en el momento de consumirlos. Utilicemos como ejemplo un componente que se encargue de renderizar el encabezado de un Post y que utilice nuestro componente Avatar para mostrar la información del autor:

// PostHeader.jsx
import gql from 'graphql-tag';
import { Avatar } from './Avatar';
export const PostHeader = ({ post }) => {
  return (
    <div>
      <Avatar user={post.author} />
      <Link to={`/post/${post.id}`}>
        <h1>{post.title}</h1>
      </Link>
    </div>
  );
};

PostHeader.fragments = {
  post: gql`
    fragment PostHeader on Post {
      id
      title
      author {
        ...Avatar
      }
    }
    ${Avatar.fragments.user}
  `
};
Enter fullscreen mode Exit fullscreen mode

Analicemos lo que está sucediendo:

  • Primero importamos el componente Avatar el cual es utilizado por PostHeader para renderizar la información del autor, pero al importar Avatar ¡también estamos importando sus Colocated Fragments por default mediante el atributo fragments!
  • A su vez, creamos un nuevo Colocated Fragment para el componente PostHeader el cual se compone de campos individuales y por el campo author el cual utiliza el Colocated Fragment ...Avatar para definir la información requerida. ¡Aquí podemos ver que la magia de la composición de React ahora también está disponible en nuestros queries!
  • Después, hacemos accesible el Colocated Fragment del componente Avatar a través de la interpolación de strings de javascript: ${Avatar.fragments.user}.
  • Por último, le pasamos a Avatar el atributo de author (que proviene del Colocated Fragment de PostHeader) a través de su prop user.

Ahora, el Colocated Fragment de PostHeader puede ser consumido de la misma manera en que consumimos el del componente Avatar, a través de su atributo fragments.

Creando un query utilizando un Colocated Fragment

Es momento de usar nuestros Colocated Fragments para construir el query. Para éste ejemplo utilizaremos el hook useQuery de @apollo/client, pero tu puedes utilizar la librería de tu preferencia:

// PostList.jsx
import { useQuery } from '@apollo/client';
import gql from 'graphql-tag';
import { PostHeader } from '../components/PostHeader';

const POST_LIST_QUERY = gql`
  query PostList {
    posts {
      ...PostHeader,
    }
  }
  ${PostHeader.fragments.post}
`;

export const PostList = () => {
  const { loading, error, data } = useQuery(POST_LIST_QUERY);
  if (loading) {
    return <div>Loading...</div>;
  }
  if (error || !data) {
    return <div>An error occurred</div>;
  }
  return (
    <div>
      <div>
        {data.posts.map((post) => (
          <PostHeader post={post} />
        ))}
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

El query es construido de la misma manera que lo hicimos para los Colocated Fragments de PostHeader y Avatar.

  • Primero importamos el componente PostHeader el cual incluye el atributo fragments. Agregamos el Colocated Fragment mediante la interpolación de strings: ${PostHeader.fragments.post} y lo consumimos dentro del cuerpo del query posts mediante ...PostHeader.
  • Nuestro query ahora incluirá todos los campos definidos en los Colocated Fragments de Avatar y PostHeader.
  • Ejecutamos el query POST_LIST_QUERY mediante el hook useQuery de @apollo/client.
  • Por último, el query posts regresa un arreglo. Iteramos el arreglo y pasamos a PostHeader cada uno de los elementos regresados por el query mediante la prop post.

Así, hemos logrado construir nuestro query exitosamente mientras mantenemos los datos requeridos unidos a los componentes que al final lo consumen.

¿Por qué utilizar Colocated Fragments?

Al utilizar Colocated Fragments, Nuestra capa de datos en GraphQL obtiene por automático alguno de los beneficios de los componentes de React:

  • Alta cohesión (High cohesion): Los componentes de React por naturaleza tienden a tener una alta cohesión, (las capas de renderizado, estilos y lógica suelen existir en el mismo archivo o carpeta). Cuando importamos un componente no debemos preocuparnos por implementar alguna de estas capas manualmente. Utilizando Colocated Fragments, tampoco debes preocuparte por cómo conseguir los datos necesaria para el componente. ¡Tu componente ahora incluye las capas de renderizado, estilo, lógica y datos!
  • Bajo acomplamiento (Low coupling): El obtener una alta cohesión entre nuestro componente y los datos nos da el beneficio adicional de obtener un bajo acoplamiento entre distintos componentes, que a su vez brinda una mayor mantenibilidad.

    Esto puede quedar más claro con un ejemplo: Digamos que nuestro componente Avatar, ahora necesita mostrar el nombre de usuario de Twitter. Así quedaría el cambio al utilizar Colocated Fragments:

    export const Avatar = ({ user }) => {
      return (
        <div>
          <a href={`/user/${user.id}`}>
            <h3>{user.name}</h3>
            {/* 1. in order to get access to this twitter attr */} 
            <h4>{user.twitter}</h4>
            <img src={user.image} />
          </a>
        </div>
      );
    };
    
    Avatar.fragments = {
      user: gql`
        fragment Avatar on User {
          id
          name
          twitter // 2. we only need to add this here
          image
        }
      `
    };
    

    Con Colocated Fragments solo necesitamos agregar el nuevo valor de twitter en la definición del Fragment y ¡listo! No necesitamos verificar que cada uno de los componentes que consumen Avatar le pasen éste nuevo atributo.

  • Composición: Mediante el uso de Colocated Fragments, logramos construir nuestros queries de la misma manera en que construimos nuestros componentes de React, mediante composición. Cada fragmento de información es exportable y reutilizable por otros fragmentos de información o queries.

Extra (typescript): Genera tus prop types automáticamente

Si utilizas typescript, hay un beneficio extra de utilizar Colocated Fragments: ¡Poder generar automáticamente los prop types de tus componentes basado en los campos de tu fragment!

Veámos como lograrlo con yarn (también es posible con npm)

  • Instalamos las liberías requeridas por @graphql-codegen

    yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations
    
  • En la carpeta raíz de tu proyecto de React/typescript ejecuta:

    ./node_modules/.bin/graphql-codegen init
    
  • Contesta las preguntas del CLI para generar el archivo de configuración:

    • What type of application are you building? React
    • Where is your schema? La url o el filepath de donde se encuentra tu schema de GraphQL
    • Where are your operations and fragments? El path regex donde se encuentran tus componentes de React. Ejemplo: ./src/**/!(*.d).{ts,tsx}
    • Pick plugins: Selecciona TypeScript y TypeScript Operations
    • Where to write the output: El path donde se generarán los prop types. Por default en src/generated/graphql.ts
    • Do you want to generate an introspection file? n
    • How to name the config file? El nombre del archivo de configuración que será creado. Por default codegen.yml
    • What script in package.json should run the codegen? El nombre del script en package.json que se generará para que ejecutes cada vez que quieras actualizar los prop types. Yo utilizo: graphql-types
  • Después de completar estas preguntas, se te habrá generado un archivo llamado codegen.yml en tu carpeta raíz como el siguiente:

    overwrite: true
    schema: "<http://localhost:4000>"
    documents: "./src/**/!(*.d).{ts,tsx}"
    generates:
      src/generated/graphql.ts:
        plugins:
          - "typescript"
          - "typescript-operations"
    
    
  • Y en tu package.json ahora deberás de tener una nueva línea en el objeto de scripts:

    "graphql-types": "graphql-codegen --config codegen.yml"
    
  • Vamos a probarlo. Ejecuta:

    yarn graphql-types
    
  • Si todo fue creado exitosamente, deberás de ver un mensaje cómo:

    yarn graphql-types
    yarn run v1.22.4
    $ graphql-codegen --config codegen.yml
      ✔ Parse configuration
      ✔ Generate outputs
    ✨  Done in 2.18s.
    
  • Después de este paso deberás encontrar un archivo src/generated/graphql.ts con todos los types generados a partir de tus Fragments y tu GraphQL Schema. Basado en nuestro ejemplo, obtenemos algo así:

    export type User = {
      __typename?: 'User';
      id: Scalars['ID'];
      name: Scalars['String'];
      email?: Maybe<Scalars['String']>;
      image?: Maybe<Scalars['String']>;
      twitter?: Maybe<Scalars['String']>;
    };
    
    export type AvatarFragment = (
      { __typename?: 'User' }
      & Pick<User, 'id' | 'name' | 'image'>
    );
    
  • Si puedes encontrar los types de tus Fragments, estás listo para comenzar a usarlos en tus componentes.

    // Avatar.tsx
    import gql from 'graphql-tag';
    import React from 'react';
    import { AvatarFragment } from '../generated/graphql';
    
    export interface AvatarProps {
        user: AvatarFragment
    }
    export const Avatar = ({ user }: AvatarProps) => {
      return (
        <div>
          <a href={`/user/${user.id}`}>
            <h3>{user.name}</h3>
            <img src={user.image} />
          </a>
        </div>
      );
    };
    
    Avatar.fragments = {
      user: gql`
        fragment Avatar on User {
          id
          name
          image
        }
      `
    };
    
  • Listo. Ahora cada que haya un cambio en tus Colocated Fragments, sólo tendrás que ejecutar yarn graphql-types y tus prop types se actualizarán automáticamente!

Por último, te dejo los links a las branches del ejemplo del Blog. Cada branch representa una forma diferente en la que puedes organizar tus queries:

Happy composing!

Discussion (0)