¿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
}
}
}
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
}
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
}
`
};
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
llamadafragments
.- 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
).
- 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
- Por último, el componente consume la información mediante la prop
user
, la cual incluye los mismos campos que el Fragment:id
,image
yname
. (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}
`
};
Analicemos lo que está sucediendo:
- Primero importamos el componente
Avatar
el cual es utilizado porPostHeader
para renderizar la información del autor, pero al importarAvatar
¡también estamos importando sus Colocated Fragments por default mediante el atributofragments
! - A su vez, creamos un nuevo Colocated Fragment para el componente
PostHeader
el cual se compone de campos individuales y por el campoauthor
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 deauthor
(que proviene del Colocated Fragment dePostHeader
) a través de su propuser
.
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>
);
};
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 atributofragments
. Agregamos el Colocated Fragment mediante la interpolación de strings:${PostHeader.fragments.post}
y lo consumimos dentro del cuerpo del queryposts
mediante...PostHeader
. - Nuestro query ahora incluirá todos los campos definidos en los Colocated Fragments de
Avatar
yPostHeader
. - Ejecutamos el query
POST_LIST_QUERY
mediante el hookuseQuery
de @apollo/client. - Por último, el query
posts
regresa un arreglo. Iteramos el arreglo y pasamos aPostHeader
cada uno de los elementos regresados por el query mediante la proppost
.
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 consumenAvatar
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
yTypeScript 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 descripts
:
"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:
- Ejemplo con Colocated Fragments y types automáticos
- Ejemplo con Colocated Fragments
- Ejemplo sin Colocated Fragments
- Ejemplo sin Colocated Fragments y todos los queries en un único archivo
Happy composing!
Top comments (0)