Repository: FormidableLabs/urql
Pull request #385 (2019-08-10 22:02:13)

Add guide on using urql outside of React.
This PR is currently WiP, but I figured I'd get feedback early from folks. The purpose of this PR is to create a guide for using `urql` outside of React. Specifically, I'm focusing on highlighting how to use `urql`'s `client` directly, since that's the easiest way for non-React consumers to get access to the core functionality.

Currently, I have two examples for using `urql` with NodeJS. I'd like to add a basic example with Vue as well before merging.

The biggest issue I see with using `urql` in non-React codebases is the peer dependency on React, and the way our internal references to `react` require it being installed even in a Node or Vue environment where it isn't used. I'd be curious to see if our tree-shaking works out in a Vue environment. Regardless, this makes me think of two things:

1. We should probably have a Caveats section in this doc that makes this plain. It's not a big deal if you're just using `urql` to write quick little Node scripts / CLIs for personal use, but the peerDep on `react` makes me question how useful this is.
2. As @kitten suggested in #253, there may be a time in the future where we consider splitting the `client` into a separate package to support non-React consumers who just want access to the client. But I don't think we're there yet 😅 We should continue to focus on making this the best it can be for React.

parkerziegler: > Do we want this to even include the HTTP server bits? This doesn't showcase a spec- compliant GraphQL server, so it may be a confusing example

I'll remove these. They're included purely because `http` is used in the CodeSandbox examples to write a response to the client (so that the user sees something), but I agree that they're just implementation details that are confusing in this context 👍 
dusty: I've just been experimenting with urql.  We have an app that is angular 1 and react.  All new things are in react, but we have a ton of old angular components that we don't have time to rewrite.  Here is what I ended up using in my experimentation.  So far I just played with queries.

I export my urql client that I created and then pull that in to an angular factory.

```
import angular from 'angular'
import { pipe, subscribe } from 'wonka'
import { createRequest } from 'urql'
import { client } from '@/graphql'  // this is the client I created

angular.module('common').factory('GraphQL', GraphQL)

GraphQL.$inject = ['$q']
function GraphQL($q) {
  return { query }

  function query({ query, variables }) {
    const request = createRequest(query, variables)
    return $q(resolve =>
      pipe(
        client.executeQuery(request),
        subscribe(resolve)
      )
    ).then(({ data, error }) => (error ? $q.reject(error) : data))
  }
}
```

Somewhere else

```
return GraphQL.query({
      query,
      variables: { id }
    }).then(data => data.theThing)
```

If we ever decide to move forward with urql (we are currently playing with apollo) I'd be happy to share whatever we end up doing.

But, it sounds like you were asking for feedback, so I thought I'd pass this along.
parkerziegler: Thanks for the example @dusty. I'm glad to see that folks already seem to be figuring a lot of what this guide will try to formalize. I think we're currently planning to keep the examples to Node and Vue, but if there's more interest I'd definitely be happy to have an Angular or Ember example in there as well; I'd probably just want to ensure we were showing examples in the latest versions of those frameworks.

Patch:
diff --git a/docs/extending-and-experimenting.md b/docs/extending-and-experimenting.md
index 72cd533e..83b045d9 100644
--- a/docs/extending-and-experimenting.md
+++ b/docs/extending-and-experimenting.md
@@ -20,6 +20,7 @@ and hooks or even React at all.
 The Client is structured so that it can easily be reused to create different
 APIs or integrate `urql` with other libraries and frameworks than React.
 [Have a look at the API docs to see a full list of the client's methods.](api.md#client-class)
+Also take a look at [how to use `urql` outside of a React context.](url-outside-react.md)
 
 While it's possible to write new APIs outside of React or new components
 quite easily, this section will only focus on hooks, for illustrative
diff --git a/docs/guides.md b/docs/guides.md
index d4ba77ed..43e283ed 100644
--- a/docs/guides.md
+++ b/docs/guides.md
@@ -411,3 +411,103 @@ by adding it into your exchanges array as `refreshTokenExchange()`.
 to run the example you'll have to open the server template.
 
 [Server template](https://codesandbox.io/s/urql-issue-template-server-0ufyz)
+
+## Adjusting an exchange
+
+Let's say the `fetchExchange` doesn't cut it for you and you want to be able to upload files,
+the first thing to do is go to the [urql exchanges](https://github.com/FormidableLabs/urql/tree/master/src/exchanges)
+find the exchange you want to change and copy it over.
+
+In our case we want to alter the `fetchExchange` to handle file uploads.
+To accomplish this we'll use an [external dependency](https://www.npmjs.com/package/extract-files)
+to extract the files from our variables.
+
+First things first, we'll need to check if the operation passed into our exchange
+contains any files. Here we'll need to alter somethings in the `createFetchSource`.
+
+```js
+import { extractFiles } from 'extract-files';
+
+...
+const createFetchSource = (operation) => {
+  return make(([next, complete]) => {
+    const { context } = operation;
+    const { clone, files } = extractFiles(operation.variables);
+    const isFileUpload = !!files.size;
+  })
+}
+...
+
+```
+
+Now we know when it's a file upload, this enables us to alter
+the request when this is true or false.
+When it's an upload we don't need to send our request as `application/json`
+but as `FormData`.
+
+```js
+...
+const isFileUpload = !!files.size;
+
+const extraOptions =
+  typeof context.fetchOptions === 'function'
+    ? context.fetchOptions()
+    : context.fetchOptions || {};
+
+const fetchOptions = {
+  method: 'POST',
+  ...extraOptions,
+};
+
+if (isFileUpload) {
+  fetchOptions.body = new FormData()
+
+  fetchOptions.body.append(
+    'operations',
+    JSON.stringify({
+      query: print(operation.query),
+      variables: Object.assign({}, operation.variables),
+    }),
+  )
+
+  const map = {}
+  let i = 0
+  files.forEach(paths => {
+    map[++i] = paths
+  });
+
+  fetchOptions.body.append('map', JSON.stringify(map));
+
+  i = 0
+  files.forEach((paths, file) => {
+    fetchOptions.body.append(`${++i}`, file, file.name)
+  });
+} else {
+  fetchOptions.headers['content-type'] = 'application/json';
+  fetchOptions.body = JSON.stringify({
+    query: print(operation.query),
+    variables: operation.variables,
+  });
+}
+...
+```
+
+This way we have altered the `fetchOptions` variable to include the
+`File` or `FileList` and send it to the server in the right format.
+
+The only thing left to do is to include this new exchange by doing:
+
+```js
+import { createClient, dedupExchange, cacheExchange } from 'urql';
+import { myFetchExchange } from './fetchExchange';
+
+const client = createClient({
+  exchanges: [dedupExchange, cacheExchange, myFetchExchange],
+});
+```
+
+Now we are all set to use our new `fetchExchange` to upload files as
+well as do normal `JSON` requests.
+
+[Client with uploadFetchExchange](https://codesandbox.io/s/urql-upload-client-d0ozh)
+[Server to go with client](https://codesandbox.io/s/urql-upload-server-p49r8)
diff --git a/docs/urql-outside-react.md b/docs/urql-outside-react.md
new file mode 100644
index 00000000..31e841ce
--- /dev/null
+++ b/docs/urql-outside-react.md
@@ -0,0 +1,152 @@
+# `urql` Outside of React
+
+While `urql` is a GraphQL client that's mainly targeting React, apart from our React-specific APIs (i.e. the hooks and components), all other elements of the library function the same outside of a React context. In this guide, we'll take a look at using `urql` in environments other than React, including NodeJS and Vue.
+
+The most useful element for using `urql` outside of React is the `Client`, the library's central orchestrator. The `Client` handles all outgoing requests and incoming responses in `urql`, and comes along with a variety of methods to help you orchestrate communication with your GraphQL API. The most useful of these are the `execute*` methods (`executeQuery`, `executeMutation`, and `executeSubscription`).
+
+The `execute*` methods all accept an instance of a `GraphQLRequest`, which can be created by using `urql`'s `createRequest` function. `createRequest` will create a unique hash used to identify the GraphQL operation, which is used internally to track it for things like deduplication and caching.
+
+```javascript
+// load dependencies
+require('isomorphic-fetch');
+const { createClient, createRequest } = require('urql');
+const gql = require('graphql-tag');
+
+// create the urql Client
+const client = createClient({
+  url: 'https://graphbrainz.herokuapp.com/graphql',
+});
+
+// define your GraphQL query
+const query = gql`
+  query SearchArtist($search: String!) {
+    search {
+      artists(query: $search, first: 1) {
+        edges {
+          node {
+            name
+            country
+            releases(first: 10) {
+              edges {
+                node {
+                  title
+                  date
+                  media {
+                    format
+                    trackCount
+                    tracks {
+                      title
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+`;
+
+// create the urql request object
+const request = createRequest(query, { search: 'Courtney Barnett' });
+```
+
+Once you've created your request, all that's left is to execute it! When you execute a query, mutation, or subscription, `urql` returns you [a `wonka` Source](https://wonka.kitten.sh/api/sources) containing the `data` and `error` states of that request. You can use [`wonka`'s Sinks](https://wonka.kitten.sh/api/sinks) to access these like so:
+
+```javascript
+const { pipe, subscribe } = require('wonka');
+
+pipe(
+  client.executeQuery(request),
+  subscribe(({ data, error }) => {
+    if (error) {
+      console.log('Error', error);
+    }
+
+    console.log('Data', data);
+  })
+);
+```
+
+Awesome, we've successfully executed a GraphQL query on the server in NodeJS! In practice, this is more useful for writing small Node scripts or a simple CLI for interacting with a GraphQL API. But it does show off the flexibility that `urql`'s `Client` gives us!
+
+See the full CodeSandbox example [here](https://codesandbox.io/s/urql-node-1jhj8).
+
+## Doing More with the Client
+
+The `Client` comes with more methods than just the `execute*` methods. While the use cases for these additional methods are fairly specific to `urql`'s implementation, they do give you more direct control over the `Client`. Let's say, for example, that you want to reexecute an operation on a predefined interval (a form of long polling). This is fairly trivial to do by calling `Client.reexecuteOperation` with the request's `Operation` object. To create the `Operation` object, use `Client.createOperation`, passing the operation type as the first argument (`"query"`, `"mutation"`, `"subscription"`, or `"teardown"`).
+
+```javascript
+require('isomorphic-fetch');
+const { createClient, createRequest } = require('urql');
+const gql = require('graphql-tag');
+const { pipe, subscribe, interval, take } = require('wonka');
+
+const client = createClient({
+  url: 'https://graphbrainz.herokuapp.com/graphql',
+});
+
+const query = gql`
+  query SearchArtist($search: String!) {
+    search {
+      artists(query: $search, first: 1) {
+        edges {
+          node {
+            name
+            country
+            releases(first: 10) {
+              edges {
+                node {
+                  title
+                  date
+                  media {
+                    format
+                    trackCount
+                    tracks {
+                      title
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+`;
+
+const request = createRequest(query, { search: 'Courtney Barnett' });
+
+pipe(
+  client.executeQuery(request),
+  subscribe(({ data, error }) => {
+    if (error) {
+      console.log('Error', error);
+    }
+
+    console.log('Data', data);
+  })
+);
+
+pipe(
+  interval(2000),
+  take(10),
+  subscribe(n => {
+    // Call client.createRequest operation to create the Operation object.
+    // The optional third argument is a partial operation context, which allows you
+    // to change the operation's request policy or fetch options.
+    const operation = client.createRequestOperation('query', request, {
+      requestPolicy: 'network-only',
+    });
+
+    // Reexecute the operation. Active subscribers will receive updates.
+    client.reexecuteOperation(operation);
+  })
+);
+```
+
+Awesome, we've got basic long polling working, where our `Client` is reexecuting the initial request every 2 seconds. By passing `{ requestPolicy: 'network-only' }` as the third argument to `reexecuteOperation`, we ensure that the `Client` issues a new request to the GraphQL API every time rather than pulling results from the cache. You'll notice in this example we use `wonka`'s `take` operator to limit the number of times we call `reexecuteOperation` – in true long polling, you wouldn't do this.
+
+See the full CodeSanbox example [here](https://codesandbox.io/s/urql-node-polling-erkwe).
diff --git a/examples/3-ssr-with-nextjs/package.json b/examples/3-ssr-with-nextjs/package.json
index 74669652..0e2a6e60 100644
--- a/examples/3-ssr-with-nextjs/package.json
+++ b/examples/3-ssr-with-nextjs/package.json
@@ -17,7 +17,7 @@
     "graphql": "^14.1.1",
     "graphql-tag": "^2.10.1",
     "isomorphic-unfetch": "^3.0.0",
-    "next": "^8.1.0",
+    "next": "latest",
     "react": "^16.8.6",
     "react-dom": "^16.8.6",
     "react-is": "^16.8.6",
diff --git a/examples/3-ssr-with-nextjs/pages/_app.js b/examples/3-ssr-with-nextjs/pages/_app.js
index 52ce3e16..f4b1804a 100644
--- a/examples/3-ssr-with-nextjs/pages/_app.js
+++ b/examples/3-ssr-with-nextjs/pages/_app.js
@@ -1,4 +1,4 @@
-import App, { Container } from 'next/app';
+import App from 'next/app';
 import React from 'react';
 
 import withUrqlClient from '../src/with-urql-client';
@@ -8,11 +8,9 @@ class MyApp extends App {
   render() {
     const { Component, pageProps, urqlClient } = this.props;
     return (
-      <Container>
         <Provider value={urqlClient}>
           <Component {...pageProps} />
         </Provider>
-      </Container>
     );
   }
 }
diff --git a/examples/3-ssr-with-nextjs/src/with-urql-client.js b/examples/3-ssr-with-nextjs/src/with-urql-client.js
index a8af6181..798b035f 100644
--- a/examples/3-ssr-with-nextjs/src/with-urql-client.js
+++ b/examples/3-ssr-with-nextjs/src/with-urql-client.js
@@ -6,7 +6,7 @@ import initUrqlClient from './init-urql-client';
 const withUrqlClient = App => {
   return class WithUrql extends React.Component {
     static async getInitialProps(ctx) {
-      const { Component, router } = ctx;
+      const { AppTree } = ctx;
 
       // Run the wrapped component's getInitialProps function
       let appProps = {};
@@ -25,10 +25,8 @@ const withUrqlClient = App => {
 
       // Run suspense and hence all urql queries
       await ssrPrepass(
-        <App
+        <AppTree
           {...appProps}
-          Component={Component}
-          router={router}
           urqlClient={urqlClient}
         />
       );
diff --git a/package.json b/package.json
index b7a7cb01..f2d9a93b 100644
--- a/package.json
+++ b/package.json
@@ -122,6 +122,7 @@
     "rollup-plugin-buble": "^0.19.8",
     "rollup-plugin-commonjs": "^10.0.1",
     "rollup-plugin-node-resolve": "^5.2.0",
+    "rollup-plugin-replace": "^2.2.0",
     "rollup-plugin-terser": "^5.1.1",
     "rollup-plugin-typescript2": "^0.22.0",
     "terser": "^4.1.2",
@@ -135,8 +136,6 @@
   },
   "dependencies": {
     "fast-json-stable-stringify": "^2.0.0",
-    "graphql-tag": "^2.10.1",
-    "prop-types": "^15.6.0",
     "wonka": "^3.2.0"
   }
 }
diff --git a/rollup.config.js b/rollup.config.js
index 26b36e03..fed9311d 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -4,6 +4,7 @@ import nodeResolve from 'rollup-plugin-node-resolve';
 import typescript from 'rollup-plugin-typescript2';
 import buble from 'rollup-plugin-buble';
 import babel from 'rollup-plugin-babel';
+import replace from 'rollup-plugin-replace';
 import { terser } from 'rollup-plugin-terser';
 
 const pkgInfo = require('./package.json');
@@ -131,8 +132,11 @@ const makePlugins = (isProduction = false) => [
       }]
     ]
   }),
+  isProduction && replace({
+    'process.env.NODE_ENV': JSON.stringify('production')
+  }),
   isProduction ? terserMinified : terserPretty
-];
+].filter(Boolean);
 
 const config = {
   input: './src/index.ts',
@@ -167,16 +171,17 @@ export default [
   }, {
     ...config,
     plugins: makePlugins(true),
+    onwarn: () => {},
     output: [
       {
-        sourcemap: true,
+        sourcemap: false,
         legacy: true,
         freeze: false,
         file: './dist/urql.min.js',
         format: 'cjs'
       },
       {
-        sourcemap: true,
+        sourcemap: false,
         legacy: true,
         freeze: false,
         file: './dist/urql.es.min.js',
diff --git a/src/components/Query.ts b/src/components/Query.ts
index 6f0f4c8f..f33b1235 100644
--- a/src/components/Query.ts
+++ b/src/components/Query.ts
@@ -10,10 +10,9 @@ export interface QueryState<T> extends UseQueryState<T> {
   executeQuery: (opts?: Partial<OperationContext>) => void;
 }
 
-export function Query<T = any, V = any>({
-  children,
-  ...args
-}: QueryProps<T, V>): ReactElement<any> {
-  const [state, executeQuery] = useQuery<T, V>(args);
-  return children({ ...state, executeQuery });
+export function Query<T = any, V = any>(
+  props: QueryProps<T, V>
+): ReactElement<any> {
+  const [state, executeQuery] = useQuery<T, V>(props);
+  return props.children({ ...state, executeQuery });
 }
diff --git a/src/components/Subscription.ts b/src/components/Subscription.ts
index 0cac3b02..cfc86dcb 100644
--- a/src/components/Subscription.ts
+++ b/src/components/Subscription.ts
@@ -12,11 +12,9 @@ export interface SubscriptionProps<T, R, V> extends UseSubscriptionArgs<V> {
   children: (arg: UseSubscriptionState<R>) => ReactElement<any>;
 }
 
-export function Subscription<T = any, R = T, V = any>({
-  children,
-  handler,
-  ...args
-}: SubscriptionProps<T, R, V>): ReactElement<any> {
-  const [state] = useSubscription<T, R, V>(args, handler);
-  return children(state);
+export function Subscription<T = any, R = T, V = any>(
+  props: SubscriptionProps<T, R, V>
+): ReactElement<any> {
+  const [state] = useSubscription<T, R, V>(props, props.handler);
+  return props.children(state);
 }
diff --git a/src/exchanges/cache.ts b/src/exchanges/cache.ts
index ce37aa94..a5279c81 100755
--- a/src/exchanges/cache.ts
+++ b/src/exchanges/cache.ts
@@ -56,9 +56,8 @@ export const cacheExchange: Exchange = ({ forward, client }) => {
       sharedOps$,
       filter(op => !shouldSkip(op) && isOperationCached(op)),
       map(operation => {
-        const { key, context } = operation;
-        const cachedResult = resultCache.get(key);
-        if (context.requestPolicy === 'cache-and-network') {
+        const cachedResult = resultCache.get(operation.key);
+        if (operation.context.requestPolicy === 'cache-and-network') {
           reexecuteOperation(client, operation);
         }
 
@@ -135,7 +134,9 @@ export const afterMutation = (
   collectTypesFromResponse(response.data).forEach(typeName => {
     const operations =
       operationCache[typeName] || (operationCache[typeName] = new Set());
-    operations.forEach(key => pendingOperations.add(key));
+    operations.forEach(key => {
+      pendingOperations.add(key);
+    });
     operations.clear();
   });
 
diff --git a/src/exchanges/debug.ts b/src/exchanges/debug.ts
index c4db97ac..66048d52 100644
--- a/src/exchanges/debug.ts
+++ b/src/exchanges/debug.ts
@@ -2,15 +2,19 @@ import { pipe, tap } from 'wonka';
 import { Exchange } from '../types';
 
 export const debugExchange: Exchange = ({ forward }) => {
-  return ops$ =>
-    pipe(
-      ops$,
-      // eslint-disable-next-line no-console
-      tap(op => console.log('[Exchange debug]: Incoming operation: ', op)),
-      forward,
-      tap(result =>
+  if (process.env.NODE_ENV === 'production') {
+    return ops$ => forward(ops$);
+  } else {
+    return ops$ =>
+      pipe(
+        ops$,
         // eslint-disable-next-line no-console
-        console.log('[Exchange debug]: Completed operation: ', result)
-      )
-    );
+        tap(op => console.log('[Exchange debug]: Incoming operation: ', op)),
+        forward,
+        tap(result =>
+          // eslint-disable-next-line no-console
+          console.log('[Exchange debug]: Completed operation: ', result)
+        )
+      );
+  }
 };
diff --git a/src/exchanges/fetch.ts b/src/exchanges/fetch.ts
index cc381855..c2238105 100755
--- a/src/exchanges/fetch.ts
+++ b/src/exchanges/fetch.ts
@@ -2,7 +2,7 @@
 import { print } from 'graphql';
 import { filter, make, merge, mergeMap, pipe, share, takeUntil } from 'wonka';
 import { Exchange, Operation, OperationResult } from '../types';
-import { addMetadata, CombinedError } from '../utils';
+import { addMetadata, makeResult, makeErrorResult } from '../utils';
 
 /** A default exchange for fetching GraphQL requests. */
 export const fetchExchange: Exchange = ({ forward }) => {
@@ -41,7 +41,10 @@ export const fetchExchange: Exchange = ({ forward }) => {
 };
 
 const createFetchSource = (operation: Operation) => {
-  if (operation.operationName === 'subscription') {
+  if (
+    process.env.NODE_ENV !== 'production' &&
+    operation.operationName === 'subscription'
+  ) {
     throw new Error(
       'Received a subscription operation in the httpExchange. You are probably trying to create a subscription. Have you added a subscriptionExchange?'
     );
@@ -79,12 +82,16 @@ const createFetchSource = (operation: Operation) => {
 
     executeFetch(operation, fetchOptions).then(result => {
       if (result !== undefined) {
-        next({
-          ...result,
-          operation: addMetadata(result.operation, {
-            networkLatency: Date.now() - startTime,
-          }),
-        });
+        if (process.env.NODE_ENV !== 'production') {
+          next({
+            ...result,
+            operation: addMetadata(result.operation, {
+              networkLatency: Date.now() - startTime,
+            }),
+          });
+        } else {
+          next(result);
+        }
       }
 
       complete();
@@ -99,50 +106,26 @@ const createFetchSource = (operation: Operation) => {
 };
 
 const executeFetch = (operation: Operation, opts: RequestInit) => {
-  let response: Response | undefined;
   const { url, fetch: fetcher } = operation.context;
 
+  let response: Response | undefined;
+
   return (fetcher || fetch)(url, opts)
     .then(res => {
+      const { status } = res;
+      const statusRangeEnd = opts.redirect === 'manual' ? 400 : 300;
       response = res;
-      checkStatus(opts.redirect, res);
-      return res.json();
+
+      if (status < 200 || status >= statusRangeEnd) {
+        throw new Error(res.statusText);
+      } else {
+        return res.json();
+      }
     })
-    .then(result => ({
-      operation,
-      data: result.data,
-      error: Array.isArray(result.errors)
-        ? new CombinedError({
-            graphQLErrors: result.errors,
-            response,
-          })
-        : undefined,
-      extensions:
-        typeof result.extensions === 'object' && result.extensions !== null
-          ? result.extensions
-          : undefined,
-    }))
+    .then(result => makeResult(operation, result, response))
     .catch(err => {
-      if (err.name === 'AbortError') {
-        return undefined;
+      if (err.name !== 'AbortError') {
+        return makeErrorResult(operation, err, response);
       }
-
-      return {
-        operation,
-        data: undefined,
-        error: new CombinedError({
-          networkError: err,
-          response,
-        }),
-        extensions: undefined,
-      };
     });
 };
-
-const checkStatus = (redirectMode: string = 'follow', response: Response) => {
-  const statusRangeEnd = redirectMode === 'manual' ? 400 : 300;
-
-  if (response.status < 200 || response.status >= statusRangeEnd) {
-    throw new Error(response.statusText);
-  }
-};
diff --git a/src/exchanges/subscription.ts b/src/exchanges/subscription.ts
index 86846484..3c7eb2e7 100644
--- a/src/exchanges/subscription.ts
+++ b/src/exchanges/subscription.ts
@@ -11,7 +11,7 @@ import {
   takeUntil,
 } from 'wonka';
 
-import { CombinedError } from '../utils/error';
+import { makeResult, makeErrorResult } from '../utils';
 
 import {
   Exchange,
@@ -72,39 +72,12 @@ export const subscriptionExchange = ({
     });
 
     return make<OperationResult>(([next, complete]) => {
-      // TODO: The conversion of the result here is very similar to fetch;
-      // We can maybe extract the logic into generic GraphQL utilities
       const sub = observableish.subscribe({
-        next: result =>
-          next({
-            operation,
-            data: result.data || undefined,
-            error: Array.isArray(result.errors)
-              ? new CombinedError({
-                  graphQLErrors: result.errors,
-                  response: undefined,
-                })
-              : undefined,
-            extensions:
-              typeof result.extensions === 'object' &&
-              result.extensions !== null
-                ? result.extensions
-                : undefined,
-          }),
-        error: err =>
-          next({
-            operation,
-            data: undefined,
-            extensions: undefined,
-            error: new CombinedError({
-              networkError: err,
-              response: undefined,
-            }),
-          }),
+        next: result => next(makeResult(operation, result)),
+        error: err => next(makeErrorResult(operation, err)),
         complete,
       });
 
-      // NOTE: Destructuring sub is avoided here to preserve its potential binding
       return () => sub.unsubscribe();
     });
   };
diff --git a/src/utils/error.ts b/src/utils/error.ts
index f26fc609..c80d8cd5 100644
--- a/src/utils/error.ts
+++ b/src/utils/error.ts
@@ -6,8 +6,7 @@ const generateErrorMessage = (
 ) => {
   let error = '';
   if (networkErr !== undefined) {
-    error = `[Network] ${networkErr.message}`;
-    return error;
+    return (error = `[Network] ${networkErr.message}`);
   }
 
   if (graphQlErrs !== undefined) {
diff --git a/src/utils/hash.ts b/src/utils/hash.ts
new file mode 100644
index 00000000..58ab442c
--- /dev/null
+++ b/src/utils/hash.ts
@@ -0,0 +1,14 @@
+// When we have separate strings it's useful to run a progressive
+// version of djb2 where we pretend that we're still looping over
+// the same string
+export const phash = (h: number, x: string): number => {
+  h = h | 0;
+  for (let i = 0, l = x.length | 0; i < l; i++) {
+    h = (h << 5) + h + x.charCodeAt(i);
+  }
+
+  return h;
+};
+
+// This is a djb2 hashing function
+export const hash = (x: string): number => phash(5381 | 0, x) >>> 0;
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 49861b08..7d2e909d 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,6 +1,6 @@
 export * from './error';
-export * from './keyForQuery';
 export * from './request';
+export * from './result';
 export * from './typenames';
 export * from './toSuspenseSource';
 
diff --git a/src/utils/keyForQuery.ts b/src/utils/keyForQuery.ts
deleted file mode 100644
index 7ff9b5b0..00000000
--- a/src/utils/keyForQuery.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import stringify from 'fast-json-stable-stringify';
-import { DocumentNode, print } from 'graphql';
-
-interface NameCache {
-  [key: string]: string;
-}
-
-interface WithNameProperty {
-  name?: { value: string };
-  [key: string]: any;
-}
-
-interface WithCacheProperty {
-  __key?: number;
-  [key: string]: any;
-}
-
-const hash = (x: string): number => {
-  /* prettier-ignore */
-  for (var h = 5381 | 0, i = 0, l = x.length | 0; i < l; i++)
-    h = ((h << 5) + h) + x.charCodeAt(i);
-  return h >>> 0;
-};
-
-const docNameCache = Object.create(null) as NameCache;
-
-export const getKeyForQuery = (doc: DocumentNode): number => {
-  if ((doc as WithCacheProperty).__key !== undefined) {
-    return (doc as WithCacheProperty).__key as number;
-  }
-
-  // Using print() can be expensive, so we just check the definition nodes' names
-  let name = doc.definitions.reduce((acc, definition) => {
-    const node = definition as WithNameProperty;
-    return acc + (node.name !== undefined ? node.name.value : '');
-  }, '');
-
-  // This is for inputs that are not using constant references. In such a case
-  // we use the Document names, but we can't use those if they're not unique
-  if (process.env.NODE_ENV !== 'production' && name !== '') {
-    const printed = print(doc);
-    if (!(name in docNameCache)) {
-      docNameCache[name] = printed;
-    } else if (docNameCache[name] !== printed) {
-      console.warn(
-        'Warning: Encountered multiple DocumentNodes with the same name.'
-      );
-    }
-  }
-
-  // If no name is present the stringified document is used
-  if (name === '') {
-    name = print(doc);
-  }
-
-  const key = hash(name);
-  (doc as WithCacheProperty).__key = key;
-  return key;
-};
-
-export const getKeyForRequest = (
-  query: DocumentNode,
-  vars?: object
-): number => {
-  const docKey = getKeyForQuery(query);
-  if (vars === undefined || vars === null) {
-    return docKey;
-  }
-
-  return hash('' + docKey + stringify(vars));
-};
diff --git a/src/utils/request.ts b/src/utils/request.ts
index a81f8763..f5ab63c3 100644
--- a/src/utils/request.ts
+++ b/src/utils/request.ts
@@ -1,16 +1,39 @@
-import { DocumentNode } from 'graphql';
-import gql from 'graphql-tag';
-import { getKeyForRequest } from './keyForQuery';
+import { DocumentNode, parse, print } from 'graphql';
+import { hash, phash } from './hash';
+import stringify from 'fast-json-stable-stringify';
 import { GraphQLRequest, Operation, OperationContext } from '../types';
 
+interface Documents {
+  [key: number]: DocumentNode;
+}
+
+const hashQuery = (q: string): number => hash(q.replace(/[\s,]+/g, ' ').trim());
+
+const docs: Documents = Object.create(null);
+const keyProp = '__key';
+
 export const createRequest = (
   q: string | DocumentNode,
   vars?: object
 ): GraphQLRequest => {
-  const query = typeof q === 'string' ? gql([q]) : q;
+  let key: number;
+  let query: DocumentNode;
+  if (typeof q === 'string') {
+    key = hashQuery(q);
+    query = docs[key] !== undefined ? docs[key] : parse(q);
+  } else if ((q as any)[keyProp] !== undefined) {
+    key = (q as any)[keyProp];
+    query = q;
+  } else {
+    key = hashQuery(print(q));
+    query = docs[key] !== undefined ? docs[key] : q;
+  }
+
+  docs[key] = query;
+  (query as any)[keyProp] = key;
 
   return {
-    key: getKeyForRequest(query, vars),
+    key: vars ? phash(key, stringify(vars)) >>> 0 : key,
     query,
     variables: vars || {},
   };
diff --git a/src/utils/result.ts b/src/utils/result.ts
new file mode 100644
index 00000000..501cc65a
--- /dev/null
+++ b/src/utils/result.ts
@@ -0,0 +1,35 @@
+import { Operation, OperationResult } from '../types';
+import { CombinedError } from './error';
+
+export const makeResult = (
+  operation: Operation,
+  result: any,
+  response?: any
+): OperationResult => ({
+  operation,
+  data: result.data,
+  error: Array.isArray(result.errors)
+    ? new CombinedError({
+        graphQLErrors: result.errors,
+        response,
+      })
+    : undefined,
+  extensions:
+    typeof result.extensions === 'object' && result.extensions !== null
+      ? result.extensions
+      : undefined,
+});
+
+export const makeErrorResult = (
+  operation: Operation,
+  error: Error,
+  response?: any
+): OperationResult => ({
+  operation,
+  data: undefined,
+  error: new CombinedError({
+    networkError: error,
+    response,
+  }),
+  extensions: undefined,
+});
diff --git a/src/utils/typenames.ts b/src/utils/typenames.ts
index 731df97d..d4979b5b 100755
--- a/src/utils/typenames.ts
+++ b/src/utils/typenames.ts
@@ -13,7 +13,9 @@ interface EntityLike {
 
 const collectTypes = (obj: EntityLike | EntityLike[], types: string[] = []) => {
   if (Array.isArray(obj)) {
-    obj.forEach(inner => collectTypes(inner, types));
+    obj.forEach(inner => {
+      collectTypes(inner, types);
+    });
   } else if (typeof obj === 'object' && obj !== null) {
     for (const key in obj) {
       if (Object.prototype.hasOwnProperty.call(obj, key)) {
diff --git a/yarn.lock b/yarn.lock
index 0308f884..a4e398f4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4816,15 +4816,6 @@ prop-types-exact@^1.2.0:
     object.assign "^4.1.0"
     reflect.ownkeys "^0.2.0"
 
-prop-types@^15.6.0, prop-types@^15.7.2:
-  version "15.7.2"
-  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
-  integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
-  dependencies:
-    loose-envify "^1.4.0"
-    object-assign "^4.1.1"
-    react-is "^16.8.1"
-
 prop-types@^15.6.2:
   version "15.6.2"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
@@ -4833,6 +4824,15 @@ prop-types@^15.6.2:
     loose-envify "^1.3.1"
     object-assign "^4.1.1"
 
+prop-types@^15.7.2:
+  version "15.7.2"
+  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
+  integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
+  dependencies:
+    loose-envify "^1.4.0"
+    object-assign "^4.1.1"
+    react-is "^16.8.1"
+
 psl@^1.1.24:
   version "1.1.32"
   resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.32.tgz#3f132717cf2f9c169724b2b6caf373cf694198db"
@@ -5279,6 +5279,14 @@ rollup-plugin-node-resolve@^5.2.0:
     resolve "^1.11.1"
     rollup-pluginutils "^2.8.1"
 
+rollup-plugin-replace@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz#f41ae5372e11e7a217cde349c8b5d5fd115e70e3"
+  integrity sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==
+  dependencies:
+    magic-string "^0.25.2"
+    rollup-pluginutils "^2.6.0"
+
 rollup-plugin-terser@^5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-5.1.1.tgz#e9d2545ec8d467f96ba99b9216d2285aad8d5b66"
@@ -5300,7 +5308,7 @@ rollup-plugin-typescript2@^0.22.0:
     rollup-pluginutils "2.8.1"
     tslib "1.10.0"
 
-rollup-pluginutils@2.8.1, rollup-pluginutils@^2.8.1:
+rollup-pluginutils@2.8.1, rollup-pluginutils@^2.6.0, rollup-pluginutils@^2.8.1:
   version "2.8.1"
   resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.1.tgz#8fa6dd0697344938ef26c2c09d2488ce9e33ce97"
   integrity sha512-J5oAoysWar6GuZo0s+3bZ6sVZAC0pfqKz68De7ZgDi5z63jOVZn1uJL/+z1jeKHNbGII8kAyHF5q8LnxSX5lQg==


Test patch:
diff --git a/src/client.test.ts b/src/client.test.ts
index 485449be..7552c29f 100755
--- a/src/client.test.ts
+++ b/src/client.test.ts
@@ -3,11 +3,6 @@ import gql from 'graphql-tag';
 
 /** NOTE: Testing in this file is designed to test both the client and it's interaction with default Exchanges */
 
-jest.mock('./utils/keyForQuery', () => ({
-  getKeyForQuery: () => 123,
-  getKeyForRequest: () => 123,
-}));
-
 import { map, pipe, subscribe, tap } from 'wonka';
 import { createClient } from './client';
 
diff --git a/src/utils/keyForQuery.test.ts b/src/utils/keyForQuery.test.ts
deleted file mode 100644
index b94914aa..00000000
--- a/src/utils/keyForQuery.test.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-import gql from 'graphql-tag';
-import { getKeyForQuery, getKeyForRequest } from './keyForQuery';
-
-const consoleWarn = console.warn;
-
-beforeEach(() => {
-  console.warn = jest.fn();
-});
-
-afterAll(() => {
-  console.warn = consoleWarn;
-});
-
-it('returns consistent hashes for DocumentNodes', () => {
-  const query = gql`
-    query Test {
-      test
-    }
-  `;
-
-  const different = gql`
-    query TestB {
-      test2
-    }
-  `;
-
-  expect((query as any).__key).toBe(undefined);
-
-  expect(getKeyForQuery(query)).toBe(getKeyForQuery(query));
-  expect(getKeyForQuery(query)).not.toBe(getKeyForQuery(different));
-
-  expect((query as any).__key).toBe(getKeyForQuery(query));
-});
-
-it('guesses the same hash for similar DocumentNodes', () => {
-  const query = () => gql`
-    query Test {
-      test
-    }
-  `;
-
-  const different = () => gql`
-    query TestB {
-      test2
-    }
-  `;
-
-  expect(getKeyForQuery(query())).toBe(getKeyForQuery(query()));
-  expect(getKeyForQuery(query())).not.toBe(getKeyForQuery(different()));
-});
-
-it('guesses the same hash for unnamed but similar DocumentNodes', () => {
-  const query = () => gql`
-    {
-      test
-    }
-  `;
-
-  const different = () => gql`
-    {
-      test2
-    }
-  `;
-
-  expect(getKeyForQuery(query())).toBe(getKeyForQuery(query()));
-  expect(getKeyForQuery(query())).not.toBe(getKeyForQuery(different()));
-});
-
-it('warns about different queries having identical names', () => {
-  const query = gql`
-    query SameName {
-      test
-    }
-  `;
-
-  const different = gql`
-    query SameName {
-      test2
-    }
-  `;
-
-  // This is expected but obviously problematic
-  expect(getKeyForQuery(query)).toBe(getKeyForQuery(different));
-
-  expect(console.warn).toHaveBeenCalled();
-});
-
-it('can key variables', () => {
-  const query = gql`
-    query SameName {
-      test
-    }
-  `;
-
-  const vars = { test: true };
-
-  expect(getKeyForQuery(query)).toBe(getKeyForRequest(query, undefined));
-  expect(getKeyForRequest(query, vars)).toBe(getKeyForRequest(query, vars));
-  expect(getKeyForRequest(query)).not.toBe(getKeyForRequest(query, vars));
-  expect(getKeyForRequest(query, { test: 2 })).not.toBe(
-    getKeyForRequest(query, vars)
-  );
-});
diff --git a/src/utils/request.test.ts b/src/utils/request.test.ts
index 6e206f13..82e87f74 100644
--- a/src/utils/request.test.ts
+++ b/src/utils/request.test.ts
@@ -1,21 +1,54 @@
-import { print } from 'graphql';
+import { parse, print } from 'graphql';
 import gql from 'graphql-tag';
 import { createRequest } from './request';
 
-const doc = print(
-  gql`
+it('should hash identical queries identically', () => {
+  const reqA = createRequest('{ test }');
+  const reqB = createRequest('{ test }');
+  expect(reqA.key).toBe(reqB.key);
+});
+
+it('should hash identical DocumentNodes identically', () => {
+  const reqA = createRequest(parse('{ testB }'));
+  const reqB = createRequest(parse('{ testB }'));
+  expect(reqA.key).toBe(reqB.key);
+  expect(reqA.query).toBe(reqB.query);
+});
+
+it('should use the hash from a key if available', () => {
+  const doc = parse('{ testC }');
+  (doc as any).__key = 1234;
+  const req = createRequest(doc);
+  expect(req.key).toBe(1234);
+});
+
+it('should hash DocumentNodes and strings identically', () => {
+  const docA = parse('{ field }');
+  const docB = print(docA).replace(/\s/g, ' ');
+  const reqA = createRequest(docA);
+  const reqB = createRequest(docB);
+  expect(reqA.key).toBe(reqB.key);
+  expect(reqA.query).toBe(reqB.query);
+});
+
+it('should hash graphql-tag documents correctly', () => {
+  const doc = gql`
     {
-      todos {
-        id
-      }
+      testD
     }
-  `
-);
+  `;
+  createRequest(doc);
+  expect((doc as any).__key).not.toBe(undefined);
+});
 
 it('should return a valid query object', () => {
+  const doc = gql`
+    {
+      testE
+    }
+  `;
   const val = createRequest(doc);
 
-  expect(print(val.query)).toBe(doc);
   expect(val).toMatchObject({
     key: expect.any(Number),
     query: expect.any(Object),
@@ -24,6 +57,13 @@ it('should return a valid query object', () => {
 });
 
 it('should return a valid query object with variables', () => {
+  const doc = print(
+    gql`
+      {
+        testF
+      }
+    `
+  );
   const val = createRequest(doc, { test: 5 });
 
   expect(print(val.query)).toBe(doc);