Después de dos años de trabajar con Apollo decidí darle una oportunidad a Relay. En este post voy a hablar de clientes de graphql, beneficios, complejidad y los primeros pasos con Relay.

Índice

Antes de continuar

Para sacarle el máximo provecho a este post es necesario que sepas algunas cosas básicas sobre GraphQL y React. Acá te dejo algunos links útiles:

(Si tenés algún post/video/link para incluir mandamelo a @okbel)

Qué es un cliente de GraphQL?

Un cliente de GraphQL es el que se encarga de interactuar con el server de GraphQL. Hacer consultas, mutaciones y más. Envía un request expresando la necesidad y recibe un JSON con la información. Esto es lo que básicamente debe cumplir.

Por qué necesitamos un cliente de GraphQL?

Esta pregunta me la hice mucho inicialmente. Si sólo querés hacer consultas a un endpoint de graphql, no necesesitás ningún cliente. Con sólo hacer una consulta POST al endpoint y enviar tu query en el body ya estás.

Con curl se vería así la mínima expresión de un cliente graphql:

curl -XPOST -H "Content-Type:application/json" -d 'query hello { world }' http://localhost:3000/graph

Ahora cuando tu aplicación crece en complejidad hay varios conceptos que vas a necesitar tener en cuenta.

Hasta ahora podemos diferenciar 2 tipos de clientes graphql. Los más simples, los que realizan consultas y mutaciones (queries-mutations) y los que a esto le suman un sistema de caching y/o herramientas para mantener consistente el estado en el cliente.

Los clientes más avanzados tratan de reducir la cantidad de roundtrips al server, tienen utilidades para manejar el estado de la información en el cliente e intentan aprovechar al máximo los beneficios que brinda graphql en el frontend.

Un ejemplo de cómo utilizar un poderoso concepto de graphql en frontend sería el uso de fragments para especificar que un componente requiere un fragmento de nuestro gran universo de datos, el schema.

Clientes de GraphQL

Hay varios clientes de graphql dando vueltas. Ahora que ya hablamos de las diferencias, van a poder distinguir los más sofisticados de los más simples. Entre ellos están:

En este post vamos a explorar a Relay:

Qué es Relay?

Relay es un framework open source de Facebook. Nos permite crear interfaces en React que especifiquen declarativamente la información que se necesita. Cada componente puede especificar su necesidad de información.

Relay se va a encargar de contactar al servidor y asegurarse de que la información esté disponible para cuando sea necesaria.

Por qué Relay?

Relay respeta los principios de React y tiene un sistema muy sofisticado diseñado para maximizar la eficiencia. Si bien su curva de aprendizaje es alta sus beneficios valen la pena.

Una de las razones por las que elijo explorar Relay es por su ahead-of-time compilation. Relay Modern (la versión actual de Relay) hace la compilación estática y no acepta interpolación. Es decir todos los template literals de graphql serán transformados.

Todas las queries son estáticas y pueden ser determinadas en build time. Esto trae muchos beneficios porque sabemos toda la necesidad que nuestra aplicación va a tener a lo largo de su vida. Es decir, mientras se encuentre ejecutada.

Gracias a todo esta compilación ahead of time Relay puede hacer muchas optimizaciones y cuando tenés una aplicación grande y fragmentada en muchos componentes activos, esto es clave.

Voy a detallar una a una las razones:

Real Colocation

La información está dónde la pedís. Expresás la necesidad en tu componente y Relay se encarga de que la información esté para cuando sea necesaria. Esto puede ser expresado con fragments donde especificás qué porción de tu schema representa tu componente.

Optimizaciones

Gracias a estas colocations y a no tener leeky props (toda la data en todos lados) se pueden hacer optimizaciones considerables. Por ejemplo, shallow comparisons ya que confiás en que se actualiza sólo lo necesario.

Performance

Relay está diseñado para trabajar con static queries, y empujar a hacer más trabajo en build/compilation time en lugar de runtime.

Persisted Queries

Este es un problema que se da cuando las queries son tan grandes que impactan en el peso del request/payload. Relay modern soporta persisted queries para hacer que el tamaño del request no sea tan grande, con esto poder enviar un id que represente la query en cuestión.

Garbage Collector 🙌

Relay automáticamente remueve data cacheada que ya no es necesaria. Esto ayuda a reducir el uso de memoria.

Primeros pasos con Relay

Lo primero que hacemos es crear nuestra aplicación React. Para eso voy a usar create-react-app

create-react-app relay-starter

A fines prácticos vamos a ejectar así tenemos control total de la aplicación.

yarn run eject

Instalamos Relay, su compiler y graphql:

yarn add react-relay relay-compiler graphql

Agregamos el script para compilar nuestra aplicación en nuestro package.json

"scripts": {
  "relay": "relay-compiler --src ./src --schema ./schema.graphql"
}
  • src : Es la carpeta donde está nuestra aplicación
  • schema: Nuestro schema graphql

Acuerdensé que el schema lo va a utilizar el compiler para entender nuestro universo de datos. Por ejemplo, va a asegurarse de que lo que le pidamos al server sea válido.

Vamos a necesitar un plugin de babel para entender las queries de graphql, así que instalamos el babel plugin:

yarn add --dev babel-plugin-relay

Agregamos relay a nuestra lista de babel plugins en nuestro package.json

  "babel": {
    "presets": [
      "react-app"
    ],
    "plugins": [
      "relay"
    ]
  },

Listo.

Corremos nuestra aplicación: yarn start

Corremos Relay: yarn run relay -- --watch

El repositorio de ejemplo va a tener un pequeño server de graphql pero pueden usar algún otro servicio online como graphcool o alguna API que tenga un endpoint de graph publico.

Environment - Creando nuestro conector al server

Tenemos que crear el environment. Acá es donde especificamos cómo el cliente se va a conectar al server y algunos datos más, como nuestro Store.

Creamos un environment.js y …

import { Environment, Network, RecordSource, Store } from 'relay-runtime';

function fetchQuery(operation, variables) {
  return fetch('/graph', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      query: operation.text,
      variables,
    }),
  }).then((response) => {
    return response.json();
  });
}

const environment = new Environment({
  network: Network.create(fetchQuery),
  store: new Store(new RecordSource()),
});

export default environment;

Environment necesita mínimamente 2 cosas para funcionar:

  • network: Acá pasamos una fetchQuery. fetchQuery es una consulta POST que se va a enviar al server. (Acá es donde enviamos la query de graphql)

  • store: Acá especificamos dónde vamos a guardar toda la client-side data.

Con esto ya estamos casi listos para traer información de nuestro server. Veamos antes nuestro schema:

Schema - Nuestro universo de datos

Nuestro schema es donde especificamos nuestro universo de datos. Para este ejemplo vamos a hacer uno simple. Este cuando preguntemos por me nos devolverá nuestro usuario.

type User {
  id: ID!
  firstName: String!
  lastName: String!
  username: String!
  website: String!
  country: String!
}

type RootQuery {
  me: User
}

schema {
  query: RootQuery
}

Queries - Consultando datos

Para hacer queries al server Relay tiene un componente que se llama QueryRenderer. Vamos a ver cómo se usa:

import React from 'react';
import { graphql, QueryRenderer } from 'react-relay';
import environment from './environment';

class App extends React.Component {
  render() {
    return (
      <QueryRenderer
        environment={environment}
        query={graphql`
          query AppQuery {
            me {
              id
            }
          }
        `}
        render={({ error, props }) => {
          if (error) {
            return <div>Error!</div>;
          }
          if (!props) {
            return <div>Loading...</div>;
          }

          return <div>User ID: {props.me.id}</div>;
        }}
      />
    );
  }
}

export default App;

Super simple, no?

Importamos el enviroment para que Relay sepa cómo hacer ese request.

Después le pasamos una query y en la prop render pasamos una función que recibe las props y cuando están listas renderizamos el contenido.

Con esto ya podés ir consultando tu graph en la aplicación.

No sé ustedes, pero a mi me gustan los hocs (Higher Order Components). Vamos a hacer un hoc para que esto se vea mejor, de paso creamos una utilidad para utilizar en toda nuestra plataforma.

HOCS - Creando nuestras utilidades

Lo primero que trato de delimitar antes de hacer una utilidad es cómo me gustaría usarla en toda la plataforma. Me imagino un hoc así:

withQuery({ query: Query })(Component)

😍

Bien, vamos a construirlo:

import React from 'react';
import { QueryRenderer } from 'react-relay';

import environment from '../environment';

export default ({ query }) => (WrappedComponent) => {
  return class WithQuery extends React.Component {
    render() {
      return (
        <QueryRenderer
          environment={environment}
          query={query}
          render={({ error, props }) => {
            if (error) {
              return <div>Error! </div>;
            }

            if (props) {
              return <WrappedComponent {...props} />;
            }

            return <div>Loading!</div>;
          }}
        />
      );
    }
  };
};

En uso se vería así:

import React from 'react';
import { graphql } from 'react-relay';
import withQuery from './hocs/withQuery';

class User extends React.Component {
  render() {
    return (
      <div>
        <ul>
          <li>ID: {this.props.me.id}</li>
          <li>
            Name: {this.props.me.firstName} {this.props.me.lastName}
          </li>
        </ul>
      </div>
    );
  }
}

export default withQuery({
  query: graphql`
    query UserQuery {
      me {
        id
        firstName
        lastName
      }
    }
  `,
})(User);

Es hermoso. Si.

Ahora podemos usar withQuery en todos los lugares de nuestra aplicación donde queramos traer información del server.

Conclusión

Relay tiene muchos beneficios a nivel técnico que no me hacen dudar a la hora de elegirlo. Pero su comunidad es pequeña y si bien parece que se mueven lento yo creo que dan pasos sólidos, confiables y útiles. Algo que me preocupaba era si esa “lentitud” me iba a tener que forzar a generar herramientas alrededor de Relay, pero creo que ese no será el caso. Relay es lo suficientemente modular como para agregarle funcionalidad sin romper o alterar su estructura.

Después de haberlo usado por una semana puedo entender porqué no hay tanta tracción. El resto de las librerías le tirás una query y un endpoint y funciona. Con Relay no es el caso. Con Relay es importante que entiendas el rol que cumple GraphQL y React para sacarle el máximo a estos conceptos. Relay no es solo un conector al server. Relay te ayuda a mantener el state server-client de una manera consistente y eficiente respetando todos los conceptos que React y GraphQL introdujeron.

Relay es un overkill para ciertos proyectos. Pero si usás caching del lado del cliente, te importa la cantidad de network trips y performance es un tema clave para tu proyecto diría que lo consideres.


Espero que con este post se animen a probarlo y si les interesa ver cosas más avanzadas de Relay en acción seguiré subiendo contenido. Persisted Queries o Subscriptions es un tema que me interesa y puedo tratar próximamente.

Acá está el repo del post : https://github.com/okbel/relay-starter.

Probablemente de una charla sobre Relay en @graphqlba.

Hay algo interesante que te gustaría saber sobre GraphQL? Después de esto tenés ganas de usar Relay? Contame: @okbel.