DEV Community

Cover image for Fullstacking: Relay + GraphQL
Mark Kop
Mark Kop

Posted on

Fullstacking: Relay + GraphQL

Now we have everything in order, we can (re)start implementing GraphQL and Relay.

I highly advise you to watch the 4 first chapters from How To Graph QL - Basics and then some reading at Relay Documentation to understand some concepts of GraphQL and Relay.

Setting Up GraphQL

First we shall define our schema.graphql. This file is written in Schema Definition Language (SDL) and contains what GraphQL will look for.
It'll usually have 3 root types: Query, Mutation and Subscription. If we set a CRUD (Create, Read, Update, Delete) style API, we'll have

  • Query: Reads
  • Mutation: Creates, Update, Delete
  • Subscription: Subscribes to these CRUD events

Besides root types, it'll also have some "object" types that will define your objects in the database.
In our case below, we're setting our schema.graphql with the Product type with a required (!) id and a title.
We're also setting a Query called "product" that needs an id and returns a Product type.
We can also set a "products" query that returns a list of Products

// packages/server/data/schema.graphql
// and a copy in packages/app/data/schema.graphql
type Product {
  id: ID!
  title: String
}

type Query {
  product(id: ID!): Product
  products: [Product]
}
Enter fullscreen mode Exit fullscreen mode

Now we have to write this schema as javascript so Koa (via koa-graphql) can use it as instructions (contract) to find data in our database.

You'll notice how some code is converted:
! as GraphQLNonNull
ID as GraphQLID
String as GraphQLString
an so on

// packages/server/graphql/productType.js
const graphql = require('graphql');
const globalIdField = require('graphql-relay').globalIdField;

const {GraphQLObjectType, GraphQLString} = graphql;

const ProductType = new GraphQLObjectType({
  name: 'Product',
  fields: () => ({
    id: globalIdField('products'),
    title: {type: GraphQLString},
  }),
});

module.exports = ProductType;
Enter fullscreen mode Exit fullscreen mode
// packages/server/graphql/schema.js
const { 
      GraphQLSchema, 
      GraphQLObjectType, 
      GraphQLID, 
      GraphQLList, 
      GraphQLNonNull,
      } = require('graphql');
const fromGlobalId = require('graphql-relay').fromGlobalId;
const productGraphQLType = require('./productType');
const Product = require('../models/Product');

const Query = new GraphQLObjectType({
  name: 'Query',
  fields: {
    product: {
      type: productGraphQLType,
      args: {id: {type: GraphQLNonNull(GraphQLID)}},
      resolve(parent, args) {
        return Product.findById(fromGlobalId(args.id).id);
      },
    },
    products: {
      type: GraphQLList(productGraphQLType),
      resolve() {
        return Product.find().lean();
      },
    },
  },
});

module.exports = new GraphQLSchema({
  query: Query,
});
Enter fullscreen mode Exit fullscreen mode

You'll notice our Resolve Functions. They are functions that connects the schema to the database. Remember that the Product class imported from '../models/Product is created with Mongoose and that's how it accesses our MongoDB instance.

React Native

To require the data from React, we'll use babel-plugin-relay/macro to "translate" graphql into our request.
We'll also use a High Order Component called <QueryRenderer> to render our actual <App> with the data from Relay.
A Query Renderer component will use the following props:

  • A configuration file Environment
  • The query
  • Variables used in the query
  • A render function that returns 3 cases: error, success and loading
// packages/app/src/App.js
import React, {Fragment} from 'react';
import {Text} from 'react-native';
import graphql from 'babel-plugin-relay/macro';
import {QueryRenderer} from 'react-relay';

import Environment from './relay/Environment';

const App = ({query}) => {
  const {products} = query;

  return (
    <Fragment>
      <Text>Hello World! Product: {products[0].title}</Text>
    </Fragment>
  );
};

const AppQR = () => {
  return (
    <QueryRenderer
      environment={Environment}
      query={graphql`
        query AppQuery {
          products {
            id
            title
          }
        }
      `}
      variables={{}}
      render={({error, props}) => {
        console.log('qr: ', error, props);
        if (error) {
          return <Text>{error.toString()}</Text>;
        }

        if (props) {
          return <App query={props} />;
        }

        return <Text>loading</Text>;
      }}
    />
  );
};

export default AppQR;
Enter fullscreen mode Exit fullscreen mode

However to make babel-plugin-relay work, you'll need to create this script to generate a schema.json file that'll be read by a relay-compiler

// packages/server/scripts/updateSchema.js
#!/usr/bin/env babel-node --optional es7.asyncFunctions

const fs = require('fs');
const path = require('path');
const schema = require('../graphql/schema');
const graphql = require('graphql').graphql;
const introspectionQuery = require('graphql/utilities').introspectionQuery;
const printSchema = require('graphql/utilities').printSchema;

// Save JSON of full schema introspection for Babel Relay Plugin to use
(async () => {
  const result = await graphql(schema, introspectionQuery);
  if (result.errors) {
    console.error(
      'ERROR introspecting schema: ',
      JSON.stringify(result.errors, null, 2),
    );
  } else {
    fs.writeFileSync(
      path.join(__dirname, '../data/schema.json'),
      JSON.stringify(result, null, 2),
    );

    process.exit(0);
  }
})();

// Save user readable type system shorthand of schema
fs.writeFileSync(
  path.join(__dirname, '../data/schema.graphql'),
  printSchema(schema),
);
Enter fullscreen mode Exit fullscreen mode

You'll need to change babel.config.js file as follows

// packages/app/babel.config.js
module.exports = {
  presets: ['module:metro-react-native-babel-preset'],
  plugins: ['macros'], // add this
};
Enter fullscreen mode Exit fullscreen mode

And you'll also need to run this updateSchema.js everytime you change your schema by using yarn update-schema

// packages/server/package.json
...
  "scripts": {
    "start": "nodemon server.js",
    "update-schema": "babel-node --extensions \".es6,.js,.es,.jsx,.mjs,.ts\" ./scripts/updateSchema.js",
    "test": "jest"
  },
...
Enter fullscreen mode Exit fullscreen mode
// package.json
...
"scripts: {
   ...
   "update-schema": "yarn --cwd packages/server update-schema",
   ...
   },
...
Enter fullscreen mode Exit fullscreen mode

Relay

The Enviroment configuration shall be done as the following:

// packages/app/src/relay/Environment.js
import {Environment, Network, RecordSource, Store} from 'relay-runtime';

import fetchQuery from './fetchQuery';

const network = Network.create(fetchQuery);

const source = new RecordSource();
const store = new Store(source);

const env = new Environment({
  network,
  store,
});

export default env;
Enter fullscreen mode Exit fullscreen mode
// packages/app/src/relay/fetchQuery.js
import {Variables, UploadableMap} from 'react-relay';
import {RequestNode} from 'relay-runtime';

export const GRAPHQL_URL = 'http://localhost:3000/graphql';

// Define a function that fetches the results of a request (query/mutation/etc)
// and returns its results as a Promise:
const fetchQuery = async (request, variables) => {
  const body = JSON.stringify({
    name: request.name, // used by graphql mock on tests
    query: request.text, // GraphQL text from input
    variables,
  });
  const headers = {
    Accept: 'application/json',
    'Content-type': 'application/json',
  };

  const response = await fetch(GRAPHQL_URL, {
    method: 'POST',
    headers,
    body,
  });

  return await response.json();
};

export default fetchQuery;
Enter fullscreen mode Exit fullscreen mode

You'll also have to configure relay-compiler by adding and running yarn relay

"scripts": {
  "relay": "relay-compiler --src ./src --schema ./schema.graphql"
}
Enter fullscreen mode Exit fullscreen mode

KoaJS

Finally, to serve our GraphQL server into a single endpoint, we'll use koa-mount and koa-graphql using our schema.js

// packages/server/server.js
const Koa = require('koa');
const mount = require('koa-mount');
const graphqlHTTP = require('koa-graphql');
const schema = require('./graphql/schema');

const databaseUrl = "mongodb://127.0.0.1:27017/test";
mongoose.connect(databaseUrl, { useNewUrlParser: true });
mongoose.connection.once("open", () => {
  console.log(`Connected to database: ${databaseUrl}`);
});

const app = new Koa();

app.use(
  mount(
    '/graphql',
    graphqlHTTP({
      schema: schema,
      graphiql: true,
    }),
  ),
);

app.listen(3000, () =>
  console.log("Server is running on http://localhost:3000/")
);
Enter fullscreen mode Exit fullscreen mode

Running

You'll need to install all dependencies first.

  • Inside app package:

yarn add react-relay
yarn add --dev graphql graphql-compiler relay-compiler relay-runtime babel-plugin-relay

  • Inside server package:

yarn add graphql koa-mount koa-graphql graphql-relay graphql-compiler
yarn add --dev @babel/core @babel/node

And run our set scripts:
yarn relay
yarn update-schema

Then you might run some yarn commands that were set in last post.

yarn start:server (don't forget to sudo service mongod start)
yarn start:app
yarn android

If you get Network error with server and mongodb running correctly, you'll need to redirect some ports with adb reverse tcp:<portnumber> tcp: <portnumber>
You may want to add the following script in packages/app/scripts/redirectPorts.sh and "redirect": "sh ./packages/app/scripts/redirectPorts.sh" in the root package.json to make things easier with a yarn redirect

adb reverse tcp:8081 tcp:8081
adb reverse tcp:3000 tcp:3000
adb reverse tcp:5002 tcp:5002

adb -d reverse tcp:8081 tcp:8081
adb -d reverse tcp:3000 tcp:3000
adb -d reverse tcp:5002 tcp:5002

adb -e reverse tcp:8081 tcp:8081
adb -e reverse tcp:3000 tcp:3000
adb -e reverse tcp:5002 tcp:5002
Enter fullscreen mode Exit fullscreen mode

That's it. You should be seeing "Stampler" in your view.

References:

Top comments (0)