├── README.md └── packages ├── .gitignore ├── client ├── README.md ├── config │ ├── env.js │ ├── jest │ │ ├── cssTransform.js │ │ └── fileTransform.js │ ├── paths.js │ ├── polyfills.js │ ├── webpack.config.dev.js │ ├── webpack.config.prod.js │ └── webpackDevServer.config.js ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── scripts │ ├── build.js │ ├── start.js │ └── test.js ├── src │ ├── App.js │ ├── apollo-client │ │ └── index.js │ ├── apollo-fetch │ │ └── index.js │ ├── fetchql │ │ └── index.js │ ├── graphql-request │ │ └── index.js │ ├── index.js │ ├── lokka │ │ └── index.js │ ├── micro-graphql-react │ │ └── index.js │ ├── queries │ │ └── index.js │ ├── relay-modern │ │ ├── __generated__ │ │ │ └── relayModernhelloQuery.graphql.js │ │ ├── environment.js │ │ └── index.js │ └── urql │ │ └── index.js └── yarn.lock └── server ├── .babelrc ├── index.js ├── package.json ├── resolvers └── index.js ├── schema ├── RootType.graphql ├── index.js └── typeDefinitions.js └── yarn.lock /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Client Exploration 2 | 3 | Checkout this in detail on https://medium.com/open-graphql/exploring-different-graphql-clients-d1bc69de305f 4 | 5 | ## Getting Started 6 | 7 | ### Starting the server 8 | 9 | `cd packages/server` 10 | 11 | `yarn start` 12 | 13 | 14 | ### Starting the client 15 | 16 | `cd packages/client` 17 | 18 | `yarn start` 19 | 20 | # Musings 21 | 22 | Over the past few weeks a lot of new GraphQL clients were released aiming to give Relay and Apollo a run for their money! It's true, there is still plenty of areas to explore when it comes to GraphQL clients and I hope the result of all this is a "melting pot" of advances in developer experience. 23 | 24 | I wanted to study how these clients work, so I aimed to implement the same query using different clients and share my experience. 25 | 26 | The GraphQL Clients we explored are: 27 | 28 | * FetchQL 29 | * GraphQL Request 30 | * Apollo Fetch 31 | * Lokka 32 | * Micro GraphQL React 33 | * URQL 34 | * Apollo Client 35 | * Relay Modern 36 | 37 | All were using React as the view layer. 38 | 39 | Before we get into the different clients I'd like to explain what exactly a GraphQL client is. 40 | 41 | If you boil it down to the basics, a GraphQL client is code that makes a POST request to a GraphQL Server. In the body of the request we send a GraphQL query or mutation as well as some variables and we expect to get some JSON back. 42 | 43 | ```graphql 44 | query example($someVariable: String) { 45 | someField(someVariable: $someVariable) { 46 | field1 47 | field2 48 | } 49 | } 50 | ``` 51 | 52 | ```bash 53 | curl -XPOST -H "Content-Type:application/json" -d 'query hello { helloWorld }' http://localhost:3000/graphql 54 | ``` 55 | 56 | From my experience there are 2 main types of GraphQL Clients. 57 | 58 | ### Fetch Client 59 | A fetch client handles sending GraphQL queries/mutation and variables to the GraphQL server in an ergonomic way for the developer. 60 | 61 | ### Caching Client 62 | A caching client does the same thing as a fetch client, but includes a way for the application to store data in memory. These clients are built to reduce network trips the application makes and provides a helping hand in managing application state. With a caching client you can keep your data layer concerns separate from your view layer. 63 | 64 | 65 | You should use a GraphQL client for work that sits at an agnostic layer of your application. You shouldn't have to worry about networking details or roll your own cache for the query results. 66 | 67 | 68 | Now that we had a brief rundown on what a GraphQL Client is, lets start exploring some clients. 69 | 70 | Our goal is make this query: 71 | 72 | ``` 73 | query hello { 74 | helloWorld 75 | } 76 | ``` 77 | 78 | and discuss our experience. 79 | 80 | // put github link 81 | 82 | ## FetchQL 83 | 84 | The first client I took a look at was FetchQL. This client is a super basic fetch client! 85 | 86 | ```js 87 | import FetchQL from "fetchql"; 88 | 89 | const client = new FetchQL({ url: "/graphql" }); 90 | 91 | client.query({ operationName: 'hello', query: helloWorldQuery }).then(result => { 92 | // do something with the result 93 | }); 94 | ``` 95 | 96 | Hooking requests in a React components `componentDidMount`, you can set state to your component pretty easily! 97 | What I really did not like about this client was the friction with adding an `operationName` to the query. Ideally 98 | you should be able to read that from the query itself, but thats okay. Still very easy to get up and running. 99 | 100 | ## GraphQL Request 101 | 102 | Next we took a look at another fetch client. `graphql-request` from my friends at graphcool is just a convenient interface over the `fetch` api. 103 | 104 | ```js 105 | import { GraphQLClient } from "graphql-request"; 106 | 107 | const client = new GraphQLClient("/graphql"); 108 | 109 | this.client.request(helloWorldQuery).then(data => { 110 | // do something 111 | }); 112 | ``` 113 | 114 | Nothing else to it! Pretty simple. I usually use this library for server to server graphql communication! Only critique on `graphql-request` is that it wasn't immediately obvious that I can pass any options `fetch` supports (it does). So all this needs is a little documentation upgrade! 115 | 116 | ## Apollo Fetch 117 | 118 | So when it comes to Apollo I am extremely biased haha. Apollo Fetch is but a small cog in the overall GraphQL client that is Apollo Client. But, you can still use it as a dead simple fetch client!! 119 | 120 | ```js 121 | import { createApolloFetch } from "apollo-fetch"; 122 | 123 | const uri = "/graphql"; 124 | // create a fetcher 125 | const fetcher = createApolloFetch({ uri }); 126 | 127 | fetcher({ query: helloWorldQuery }).then(result => { 128 | // do something 129 | }); 130 | ``` 131 | 132 | I'm starting to see a trend between all these fetch clients. Most of them use the `fetch` api and operate the same way. I'm starting to wonder why they exist? 133 | 134 | ## Micro GraphQL React 135 | Micro GraphQL is created by Adam Rackis with the aim to have a simple client to connect React components to a GraphQL Server. It uses `fetch` with `HTTP Get` for queries and uses `graphql-request` to handle mutations. It has a built in cache stored at the component level. 136 | 137 | Let's show an example: 138 | 139 | ```js 140 | import React from "react"; 141 | import { Client, query } from "micro-graphql-react"; 142 | import { helloWorldQuery } from "../queries"; 143 | 144 | const client = new Client({ 145 | endpoint: "/graphql" 146 | }); 147 | 148 | class MicroGraphQL extends React.Component { 149 | render() { 150 | const { loading, data } = this.props; 151 | 152 | if (loading) { 153 | return
Loading your MicroGraphQL data...
; 154 | } 155 | 156 | return{data && data.helloWorld} from Micro GraphQL React
; 157 | } 158 | } 159 | 160 | export default query(client, props => ({ 161 | query: helloWorldQuery 162 | }))(MicroGraphQL); 163 | ``` 164 | 165 | Instantiate a client instance like we're used to, then wrap the component in a `query` container to handle the fetch of the query and pass the `data` as props to the component. Under the hood, the component uses a `Map` to set the cache at the component level. 166 | 167 | The caching goal here is to actually use a tool like Google's Workbox, or sw-toolbox to take the response from the HTTP requests and cache results there. 168 | 169 | My critique for this client is the need to pass the client instance into the container component every time I need to make a query. Maybe with the new React Context API this can be passed a lot easier to child components! Also the caching at the component level for me is a little limiting, but this library clearly aims to solve this a certain way and thats okay! 170 | 171 | 172 | ## Lokka 173 | Lokka by my friend Arunoda was one of the first clients aside from relay classic back before even Apollo existed! It also heavily inspired some of the clients you see today. I think the distinguishing factor is its separation of the "transport" or "network" interface e.g. "over what protocol are these requests going through?" and the actual mechanism by which results are cached. When we look at caching clients today they are very modular and I give props to Arunoda for being forward thinking. When you separate the network interface from the client code you give engineers the ability to send GraphQL requests over different protocols! Like Websockets or whatever else you want! I think it's also safe to assume that if you're using a fetch client, you're probably speaking HTTP! 174 | 175 | Let's setup Lokka, it's super easy 176 | 177 | ```js 178 | import { Lokka } from "lokka"; 179 | import { Transport } from "lokka-transport-http"; 180 | 181 | const client = new Lokka({ 182 | transport: new Transport("/graphql") 183 | }); 184 | 185 | client.query(helloWorldQuery).then(result => { 186 | // do something 187 | }); 188 | ``` 189 | 190 | Lokka has a built in cache when using the `watchQuery` API. 191 | 192 | ```js 193 | // watch the query 194 | const watchHandler = (err, payload) => { 195 | if (err) { 196 | console.error(err.message); 197 | return; 198 | } 199 | 200 | // do something when the cache updates 201 | }; 202 | 203 | client.watchQuery(helloWorldQuery, {}, watchHandler); 204 | ``` 205 | 206 | Any time the Lokka cache is updated, the registered handler function is called. This allows you to do a lot of different things in your UI in response to cache updates! 207 | 208 | ## URQL 209 | URQL by Formidable Labs is a GraphQL client aiming to make the client side GraphQL workflow as simple as possible. Under the hood this uses a `fetch` api to handle the fetch client. 210 | 211 | Let's write a simple example: 212 | 213 | First we need to setup a Provider to pass the `client` instance to child components. 214 | ```js 215 | import React from "react"; 216 | import { Provider, Client } from "urql"; 217 | import { helloWorldQuery } from "../queries"; 218 | 219 | const client = new Client({ 220 | url: "/graphql" 221 | }); 222 | 223 | export default function Root() { 224 | return ( 225 |Loading data from Apollo Client...
; 310 | 311 | return{data && data.helloWorld} from Apollo Client
; 312 | }} 313 |{error.message}
; 393 | } else if (props) { 394 | return{props.helloWorld} from Relay Modern
; 395 | } 396 | returnLoading your Relay Modern data...
; 397 | }} 398 | /> 399 | ); 400 | } 401 | ``` 402 | 403 | We can see come commonalities here: 404 | 405 | 1. The use of `graphql` to take a query string and use a GraphQL AST under the hood. 406 | 2. The `QueryRenderer`, much like `Query` or `Connect` that allows you to render a component based on data. 407 | 408 | Thing that felt off: 409 | 410 | Before starting the application I needed to run the relay compiler: 411 | 412 | `relay-compiler --src ./src --schema ./schema.graphql` pointing to my schema in the server folder. If building a Relay app, you should probably keep your schema accessible in a shared place! 413 | 414 | After running this compiler I got an error! 415 | 416 | ``` 417 | Operation names in graphql tags must be prefixed with the module name and end in "Mutation", "Query", or "Subscription". Got `hello` in module `relayModern`. 418 | ``` 419 | 420 | I stared this error and was really taken a back. Seems pretty annoying to have to do this, but meh, let's keep going: 421 | 422 | Change up my query to this: 423 | 424 | ```js 425 | 426 | const query = graphql` 427 | query relayModernhelloQuery { 428 | helloWorld 429 | } 430 | `; 431 | ``` 432 | 433 | Run the compiler now, run the app, all is good! Relay is a powerful tool but I think the least approachable out of all the GraphQL Clients out there. The client has different concerns and tons of use cases to back it up, so I have no problem with its design! 434 | 435 | # Conclusion 436 | 437 | So having explored these clients I've come to realize a few things: 438 | 439 | 1. We have too many fetch clients out there. Many of the clients that just help you do POST requests via `fetch` aren't really providing any extra value. I'd probably stick to recommending `graphql-request`, or if you're more familiar with Apollo using `apollo-fetch`. 440 | 441 | 2. Clients that do support caching are relatively doing things similarly. Between Apollo, and Relay you can clearly see a modular separation of concerns between the `networking` and the `cache` details. 442 | 443 | 3. Users of clients like `URQL` and `Micro GraphQL` are looking for a client that is easy to configure and work with right away. 444 | 445 | 4. The key for new user adoption is a client that handles these concerns for you with escape hatches for customization when needed. I'm super excited for `Apollo Boost` and there's a project that is similar for relay https://github.com/releasy/react-releasy 446 | 447 | I'm so happy the community is coming together around different ideas and making things easier for engineers going forward! 448 | 449 | Cheers. 450 | -------------------------------------------------------------------------------- /packages/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /packages/client/README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Client Exploration 2 | -------------------------------------------------------------------------------- /packages/client/config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | var dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | `${paths.dotenv}.${NODE_ENV}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. Variable expansion is supported in .env files. 31 | // https://github.com/motdotla/dotenv 32 | // https://github.com/motdotla/dotenv-expand 33 | dotenvFiles.forEach(dotenvFile => { 34 | if (fs.existsSync(dotenvFile)) { 35 | require('dotenv-expand')( 36 | require('dotenv').config({ 37 | path: dotenvFile, 38 | }) 39 | ); 40 | } 41 | }); 42 | 43 | // We support resolving modules according to `NODE_PATH`. 44 | // This lets you use absolute paths in imports inside large monorepos: 45 | // https://github.com/facebookincubator/create-react-app/issues/253. 46 | // It works similar to `NODE_PATH` in Node itself: 47 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 48 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 49 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 50 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 51 | // We also resolve them to make sure all tools using them work consistently. 52 | const appDirectory = fs.realpathSync(process.cwd()); 53 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 54 | .split(path.delimiter) 55 | .filter(folder => folder && !path.isAbsolute(folder)) 56 | .map(folder => path.resolve(appDirectory, folder)) 57 | .join(path.delimiter); 58 | 59 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 60 | // injected into the application via DefinePlugin in Webpack configuration. 61 | const REACT_APP = /^REACT_APP_/i; 62 | 63 | function getClientEnvironment(publicUrl) { 64 | const raw = Object.keys(process.env) 65 | .filter(key => REACT_APP.test(key)) 66 | .reduce( 67 | (env, key) => { 68 | env[key] = process.env[key]; 69 | return env; 70 | }, 71 | { 72 | // Useful for determining whether we’re running in production mode. 73 | // Most importantly, it switches React into the correct mode. 74 | NODE_ENV: process.env.NODE_ENV || 'development', 75 | // Useful for resolving the correct path to static assets in `public`. 76 | // For example,