Repository: FormidableLabs/urql
Pull request #370 (2019-08-05 15:26:16)

(docs) - guided exchange
Making a file upload exchange guide


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f5f98fb3..1478a528 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,20 @@ If a change is missing an attribution, it may have been made by a Core Contribut
 
 _The format is based on [Keep a Changelog](http://keepachangelog.com/)._
 
+## [v1.3.0](https://github.com/FormidableLabs/urql/compare/v1.2.0...v1.3.0)
+
+This release comes with some important fixes and enhancements, which all address
+certain edge-cases when using `urql`.
+
+It fixes the `cache-and-network` request policy, which wouldn't always work correctly and issue another network request after resolving a response from the default cache. We also had a major bug in React Native environments where responses wouldn't ever be reflected in the `useQuery` hook's state. Lastly, you can now use `extensions` from your GraphQL servers and modify the `OperationContext` from the hooks options.
+
+- ✨ Add support for `extensions` key in GraphQL responses, by [@adamscybot](https://github.com/adamscybot) (see [#355](https://github.com/FormidableLabs/urql/pull/355))
+- ⚠ Fix `cache-and-network` request policy by adding operation flushing to the client (see [#356](https://github.com/FormidableLabs/urql/pull/356))
+- Add `fetch` option to the Client so it doesn't have to be polyfilled globally (see [#357](https://github.com/FormidableLabs/urql/pull/357) and [#359](https://github.com/FormidableLabs/urql/pull/359))
+- ⚠ Fix `useImmediateState` for React Native environments (see [#358](https://github.com/FormidableLabs/urql/pull/358))
+- ✨ Add `context` option to all hooks to allow `OperationContext` to be changed dynamically (see [#351](https://github.com/FormidableLabs/urql/pull/351))
+- Add `isClient` option to `ssrExchange` in case `suspense` is activated on the client-side (see [#369](https://github.com/FormidableLabs/urql/pull/369))
+
 ## [v1.2.0](https://github.com/FormidableLabs/urql/compare/v1.1.3...v1.2.0)
 
 A release focused on improving developer experience (in preparation for the
diff --git a/README.md b/README.md
index da3bb6d1..870ed3f0 100644
--- a/README.md
+++ b/README.md
@@ -47,7 +47,7 @@ While GraphQL is an elegant protocol and schema language, client libraries today
 - [Basics](https://formidable.com/open-source/urql/docs/basics/)
 - [Extending & Experimenting](https://formidable.com/open-source/urql/docs/extending-&-experimenting/)
 - [API](https://formidable.com/open-source/urql/docs/api/)
-- [Guides](https://github.com/FormidableLabs/urql/blob/docs/customExchanges/docs/guides.md)
+- [Guides](./docs/guides.md)
 
 _You can find the raw markdown files inside this repository's `docs` folder._
 
diff --git a/docs/README.md b/docs/README.md
index e4650760..e739cc5c 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,7 +1,7 @@
 # Documentation
 
-`urql` is a highly customisable and flexible GraphQL client, that
-happens to come with some default core behaviour and some React
+`urql` is a highly customizable and flexible GraphQL client, that
+happens to come with some default core behavior and some React
 component and hooks APIs.
 
 You can take `urql` from writing your first GraphQL app, to
@@ -24,7 +24,7 @@ exchange pipeline to your GraphQL API.
 
 ### [Basics](basics.md)
 
-Everything about the basic & default `urql` behaviour
+Everything about the basic & default `urql` behavior
 that comes with explanations of the `fetchExchange`,
 the `cacheExchange`, and how to use _Subscriptions_.
 
@@ -40,7 +40,7 @@ Here's everything you need to know to extend,
 customise and experiment with `urql`. This section
 shows you how to use the `Client` to write new
 APIs (Be it for React or not) and how to write
-new "Exchanges" to customise `urql`'s core behaviour!
+new "Exchanges" to customise `urql`'s core behavior!
 
 ### [Guides](guides.md)
 
diff --git a/docs/api.md b/docs/api.md
index 40c46d17..00c3bb69 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -427,9 +427,15 @@ streams `GraphQLResult`s with `data` and `errors`.
 The `ssrExchange` as [described in the Basics section](basics.md#server-side-rendering).
 It's of type `Options => Exchange`.
 
-It accepts a single input, `{ initialState }`, which is completely
+It accepts two inputs, `initialState` which is completely
 optional and populates the server-side rendered data with
-a rehydrated cache.
+a rehydrated cache, and `isClient` which can be set to
+`true` or `false` to tell the `ssrExchange` whether to
+write to (server-side) or read from (client-side) the cache.
+
+By default `isClient` defaults to `true` when the `Client.suspense`
+mode is disabled and to `false` when the `Client.suspense` mode
+is enabled.
 
 This can be used to extract data that has been queried on
 the server-side, which is also described in the Basics section,
diff --git a/docs/architecture.md b/docs/architecture.md
index ce4e9bfd..f2fe79ad 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -1,10 +1,10 @@
 # Architecture
 
-Much of `urql` is about being flexible and customisable.
+Much of `urql` is about being flexible and customizable.
 To this extent a large chunk of this document is dedicated to
 how `urql` works and how to adapt it to different use cases.
 
-If you wish to use `urql` without any customisations, this
+If you wish to use `urql` without any customizations, this
 document is entirely optional for you. But it's still worth
 the read. Promised.
 
@@ -101,7 +101,7 @@ the second argument to `executeQuery`. By default it contains:
 
 - `fetchOptions` for the `fetch` call's options
 - `url` for the `fetch` call's API endpoint
-- `requestPolicy` to determine the cache's behaviour
+- `requestPolicy` to determine the cache's behavior
 
 The `executeQuery` call will return a [Wonka](https://github.com/kitten/wonka)
 stream. This is just an observable (not following the Observable spec)
@@ -166,7 +166,7 @@ same operation / request twice at the same time.
 **Second,** operations are checked against the cache. Depending on the `requestPolicy`
 cached results can be resolved instead and results from network requests are cached.
 
-**Third,** operations are sent to the API and the result is normalised.
+**Third,** operations are sent to the API and the result is normalized.
 
 ## Operation Results
 
diff --git a/docs/basics.md b/docs/basics.md
index aeca0d6a..c7214383 100644
--- a/docs/basics.md
+++ b/docs/basics.md
@@ -1,7 +1,7 @@
 # Basics
 
 As mentioned before, `urql`'s core logic is split into exchanges.
-To that end, `urql`'s behaviour is completely defined by the exchanges
+To that end, `urql`'s behavior is completely defined by the exchanges
 you pass to it or that are the default ones.
 
 This document goes through the exchanges that `urql` adds by default.
@@ -65,12 +65,12 @@ operations and sends `POST` requests using `fetch`.
 
 ## `cacheExchange`
 
-The default caching behaviour that `urql` uses is defined by the `cacheExchange`
-unlike Apollo's `Cache` or `InMemoryCache`, caching behaviour is handled as
-part of the request pipeline, which makes customisation a lot easier as
+The default caching behavior that `urql` uses is defined by the `cacheExchange`
+unlike Apollo's `Cache` or `InMemoryCache`, caching behavior is handled as
+part of the request pipeline, which makes customization a lot easier as
 there's no extra API to learn.
 
-By default however, `urql`'s caching behaviour is not that of a _"normalising
+By default however, `urql`'s caching behavior is not that of a _"normalizing
 cache"_ but more of a _"document cache"_.
 
 ### The document cache
@@ -123,7 +123,7 @@ The operation context can also contain a `requestPolicy` property
 that alters when and how the cache responds.
 By default this will be set to `'cache-first'`.
 
-When `'cache-first'`, the default behaviour, is used, the cache
+When `'cache-first'`, the default behavior, is used, the cache
 will return all cached results when they're available. When no
 cached result is available it will let the operation through, so
 that the `fetchExchange` can send a request to the API.
@@ -150,9 +150,9 @@ from the API.
 > that data skips the cache, if it's clear to you that the result will
 > need to be up-to-date.
 
-### Customisation
+### Customization
 
-The idea of `urql` is that you can customise the caching behaviour amongst
+The idea of `urql` is that you can customise the caching behavior amongst
 other things yourself, if needed.
 
 [Read more about customising `urql` in the "Extending & Experimenting" section.](extending-and-experimenting.md)
@@ -230,6 +230,15 @@ The exchange returned by `ssrExchange()` should be added after the `cacheExchang
 (or any other custom cache exchange you've defined), and before any
 asynchronous exchanges like the `fetchExchange`.
 
+If you're also using suspense mode on the client, you can
+additionally set the `isClient` option, which tells the `ssrExchange` manually
+whether it's on the server or client, so that you can enable the `suspense`
+mode on the client-side as well.
+
+```js
+const ssrCache = ssrExchange({ isClient: !!process.browser });
+```
+
 ### Prefetching on the server
 
 In your request handler on the server-side, you'll have to add some
diff --git a/docs/exchanges.md b/docs/exchanges.md
index 1ea2d785..584d1f83 100644
--- a/docs/exchanges.md
+++ b/docs/exchanges.md
@@ -1,16 +1,18 @@
 # Exchanges
 
-## Packaged
+## Packages
 
 These exchanges can be imported from the `urql` package.
 
-- cacheExchange - caches all incoming data
-- debugExchange - gives information about outgoing graphql requests in log format
-- dedupExchange - deduplicates outgoing queries
-- fetchExchange - used to manage the outgoing graphql requests
-- subscriptionExchange - used to support subscriptions
+- `cacheExchange`: the default document cache implementation
+- `debugExchange`: logs information about ongoing operations and results
+- `dedupExchange`: deduplicates ongoing operations
+- `fetchExchange`: sends operations to GraphQL HTTP endpoints and resolves results
+- `ssrExchange`: used to cache results during SSR and rehydrate them on the client-side
+- `subscriptionExchange`: used to support GraphQL subscriptions
 
-## Third-party
+## Addons
 
-- [Persisted Queries](https://github.com/Daniel15/urql-persisted-queries) -
-  Uses persisted query hashes to send graphql-queries if possible
+- [`@urql/devtools`](https://github.com/FormidableLabs/urql-devtools): A Chrome extension for monitoring and debugging
+- [`@urql/exchange-suspense`](https://github.com/FormidableLabs/urql-exchange-suspense): An experimental exchange for using `<React.Suspense>`
+- [`urql-persisted-queries`](https://github.com/Daniel15/urql-persisted-queries): An exchange for adding persisted query support
diff --git a/docs/extending-and-experimenting.md b/docs/extending-and-experimenting.md
index d2883f45..72cd533e 100644
--- a/docs/extending-and-experimenting.md
+++ b/docs/extending-and-experimenting.md
@@ -5,12 +5,12 @@ and its [Basics](basics.md). This section will introduce you to hacking
 with `urql`.
 
 `urql` comes with some very functional defaults, but its standard component APIs,
-hook APIs, or its core behaviour might not be enough for your complex app. Or
+hook APIs, or its core behavior might not be enough for your complex app. Or
 maybe you're just looking to play around and experiment with GraphQL clients?
 
 This document contains two main sections. The first is about reusing `urql`'s
 core and build new "outward facing APIs". The second is about writing new
-exchanges and hence changing `urql`'s core behaviour.
+exchanges and hence changing `urql`'s core behavior.
 
 ## Writing new APIs for the Client
 
@@ -138,7 +138,7 @@ We have now:
 - Added the second `useEffect` argument to tell it when to rerun
 - Added state to set `fetching: true` and to update the result
 
-In the actual implementation of the hook we also generalise this
+In the actual implementation of the hook we also generalize this
 execution and expose the `executeQuery` function in the returned tuple.
 This is left out here as the implementation can be found in the source code.
 
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 479aa2f7..a428529c 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -171,7 +171,7 @@ pass in a fully parsed AST in the form of `DocumentNode` instead.
 For this purpose you can use `graphql-tag`.
 
 This can be extremely helpful, since it enables syntax highlighting
-in some editors. It also can be used to preparse the GraphQL query
+in some editors. It also can be used to pre-parse the GraphQL query
 using `babel-plugin-graphql-tag` or the included Webpack loader.
 
 You only have to make a small adjustment. Install `graphql-tag` and
@@ -353,9 +353,9 @@ cached completely. When the same query and variables are requested again,
 result is also invalidated when a mutation with similar `__typename`s was
 sent.
 
-[You can find out more about the default caching behaviour in the Basics' `cacheExchange` section.](basics.md#cacheexchange)
+[You can find out more about the default caching behavior in the Basics' `cacheExchange` section.](basics.md#cacheexchange)
 
-Using `urql`'s default behaviour this means we sometimes need a way to refetch
+Using `urql`'s default behavior this means we sometimes need a way to refetch
 data from the GraphQL API and skip the cache, if we need fresh data.
 
 The easiest way to always display up-to-date data is to set the `requestPolicy`
diff --git a/docs/guides.md b/docs/guides.md
index e2991583..43e283ed 100644
--- a/docs/guides.md
+++ b/docs/guides.md
@@ -18,7 +18,7 @@ your own exchanges by learning how we write ours.
 ## Introduction
 
 All exchanges are written with [Wonka](https://wonka.kitten.sh/), a ligtweight iterable and
-observable streaming library. Wonka is used `urql` because its behaviour
+observable streaming library. Wonka is used `urql` because its behavior
 is extremely predictable and built to be treeshakeable.
 
 [You can read more about how to use it on the Wonka site.](https://wonka.kitten.sh/basics/)
@@ -31,7 +31,7 @@ and with utilities like `map` and `filter` that transform their output,
 but they work with values that come in asynchronously over time.
 
 These utilities are called "operators" and there's a lot of them to
-enable you to express any asynchronous or iterable behaviour that
+enable you to express any asynchronous or iterable behavior that
 you need.
 
 For instance you can convert an array into a Wonka source,
@@ -208,14 +208,14 @@ asynchronicity.
 
 This may happen when you use a timing utility from Wonka, like
 [`delay`](https://wonka.kitten.sh/api/operators#delay) or [`throttle`](https://wonka.kitten.sh/api/operators#throttle)
-Or this could happen because your exchange inherently does somethingasynchronous,
+Or this could happen because your exchange inherently does something asynchronous,
 like fetch some data or use a promise.
 
 When you write exchanges, some will inevitably be asynchronous, if
 they're fetching results, performing authentication, or other tasks
 that you have to wait for.
 
-This can cause problems, because the behaviour in `urql` is built
+This can cause problems, because the behavior in `urql` is built
 to be _synchronous_ first. This helps us build our suspense mode,
 and it helps your components receive cached data on their initial
 mount without rerendering.
@@ -241,7 +241,7 @@ to put them in a specific order. For instance, an authentication exchange
 needs to go before the `fetchExchange`. And a secondary cache would
 maybe go in front of the default cache exchange.
 
-But to ensure the correct behaviour of suspense mode and
+But to ensure the correct behavior of suspense mode and
 the initialization of our hooks, it's vital to order your exchanges
 so that synchronous exchanges come first and asynchronous ones
 come last.
@@ -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/package.json b/package.json
index 2f8b9974..f2d9a93b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "urql",
-  "version": "1.2.0",
+  "version": "1.3.0",
   "description": "A highly customizable and versatile GraphQL client for React",
   "repository": "https://www.github.com/FormidableLabs/urql",
   "bugs": {
@@ -114,7 +114,7 @@
     "react": "^16.8.6",
     "react-dom": "^16.8.6",
     "react-is": "^16.8.6",
-    "react-ssr-prepass": "^1.0.5",
+    "react-ssr-prepass": "^1.0.6",
     "react-test-renderer": "^16.8.6",
     "rimraf": "^2.6.2",
     "rollup": "^1.17.0",
@@ -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.1.1"
+    "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/client.ts b/src/client.ts
index e414628d..23e354c2 100755
--- a/src/client.ts
+++ b/src/client.ts
@@ -23,6 +23,7 @@ import {
   OperationContext,
   OperationResult,
   OperationType,
+  RequestPolicy,
 } from './types';
 
 import { toSuspenseSource } from './utils';
@@ -39,6 +40,8 @@ export interface ClientOptions {
   exchanges?: Exchange[];
   /** Activates support for Suspense. */
   suspense?: boolean;
+  /** The default request policy for requests. */
+  requestPolicy?: RequestPolicy;
 }
 
 interface ActiveOperations {
@@ -55,6 +58,7 @@ export class Client {
   fetchOptions?: RequestInit | (() => RequestInit);
   exchange: Exchange;
   suspense: boolean;
+  requestPolicy: RequestPolicy;
 
   // These are internals to be used to keep track of operations
   dispatchOperation: (operation: Operation) => void;
@@ -67,6 +71,7 @@ export class Client {
     this.fetchOptions = opts.fetchOptions;
     this.fetch = opts.fetch;
     this.suspense = !!opts.suspense;
+    this.requestPolicy = opts.requestPolicy || 'cache-first';
 
     // This subject forms the input of operations; executeOperation may be
     // called to dispatch a new operation on the subject
@@ -111,7 +116,7 @@ export class Client {
   private createOperationContext = (
     opts?: Partial<OperationContext>
   ): OperationContext => {
-    const { requestPolicy = 'cache-first' } = opts || {};
+    const { requestPolicy = this.requestPolicy } = opts || {};
 
     return {
       url: this.url,
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/ssr.ts b/src/exchanges/ssr.ts
index a5afa1a3..ff5d73b2 100644
--- a/src/exchanges/ssr.ts
+++ b/src/exchanges/ssr.ts
@@ -15,6 +15,7 @@ export interface SSRData {
 }
 
 export interface SSRExchangeParams {
+  isClient?: boolean;
   initialState?: SSRData;
 }
 
@@ -77,6 +78,13 @@ export const ssrExchange = (params?: SSRExchangeParams): SSRExchange => {
   // The SSR Exchange is a temporary cache that can populate results into data for suspense
   // On the client it can be used to retrieve these temporary results from a rehydrated cache
   const ssr: SSRExchange = ({ client, forward }) => ops$ => {
+    // params.isClient tells us whether we're on the client-side
+    // By default we assume that we're on the client if suspense-mode is disabled
+    const isClient =
+      params && typeof params.isClient === 'boolean'
+        ? !!params.isClient
+        : !client.suspense;
+
     const sharedOps$ = share(ops$);
 
     let forwardedOps$ = pipe(
@@ -96,8 +104,8 @@ export const ssrExchange = (params?: SSRExchangeParams): SSRExchange => {
       })
     );
 
-    if (client.suspense) {
-      // Inside suspense-mode we cache results in the cache as they're resolved
+    if (!isClient) {
+      // On the server we cache results in the cache as they're resolved
       forwardedOps$ = pipe(
         forwardedOps$,
         tap((result: OperationResult) => {
@@ -109,7 +117,7 @@ export const ssrExchange = (params?: SSRExchangeParams): SSRExchange => {
         })
       );
     } else {
-      // Outside of suspense-mode we delete results from the cache as they're resolved
+      // On the client we delete results from the cache as they're resolved
       cachedOps$ = pipe(
         cachedOps$,
         tap((result: OperationResult) => {
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/hooks/index.ts b/src/hooks/index.ts
index 58b57faa..f7c86247 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -1,3 +1,4 @@
 export * from './useMutation';
 export * from './useQuery';
 export * from './useSubscription';
+export * from './useDevtoolsContext';
diff --git a/src/hooks/useImmediateEffect.ts b/src/hooks/useImmediateEffect.ts
index 18672e72..948eb62d 100644
--- a/src/hooks/useImmediateEffect.ts
+++ b/src/hooks/useImmediateEffect.ts
@@ -2,11 +2,11 @@
 
 import { useRef, useEffect, EffectCallback } from 'react';
 
-enum LifecycleState {
-  WillMount = 0,
-  DidMount = 1,
-  Update = 2,
-}
+type LifecycleState = 0 | 1 | 2;
+
+const WILL_MOUNT = 0;
+const DID_MOUNT = 1;
+const UPDATE = 2;
 
 /** This is a drop-in replacement for useEffect that will execute the first effect that happens during initial mount synchronously */
 export const useImmediateEffect = (
@@ -14,21 +14,21 @@ export const useImmediateEffect = (
   changes: ReadonlyArray<any>
 ) => {
   const teardown = useRef<ReturnType<EffectCallback>>(undefined);
-  const state = useRef(LifecycleState.WillMount);
+  const state = useRef<LifecycleState>(WILL_MOUNT);
 
   // On initial render we just execute the effect
-  if (state.current === LifecycleState.WillMount) {
-    state.current = LifecycleState.DidMount;
+  if (state.current === WILL_MOUNT) {
+    state.current = DID_MOUNT;
     teardown.current = effect();
   }
 
   useEffect(() => {
     // Initially we skip executing the effect since we've already done so on
     // initial render, then we execute it as usual
-    if (state.current === LifecycleState.Update) {
+    if (state.current === UPDATE) {
       return (teardown.current = effect());
     } else {
-      state.current = LifecycleState.Update;
+      state.current = UPDATE;
       return teardown.current;
     }
   }, changes);
diff --git a/src/utils/error.ts b/src/utils/error.ts
index f9470c05..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) {
@@ -54,16 +53,16 @@ export class CombinedError extends Error {
     graphQLErrors?: Array<string | GraphQLError | Error>;
     response?: any;
   }) {
-    const normalisedGraphQLErrors = (graphQLErrors || []).map(
+    const normalizedGraphQLErrors = (graphQLErrors || []).map(
       rehydrateGraphQlError
     );
-    const message = generateErrorMessage(networkError, normalisedGraphQLErrors);
+    const message = generateErrorMessage(networkError, normalizedGraphQLErrors);
 
     super(message);
 
     this.name = 'CombinedError';
     this.message = message;
-    this.graphQLErrors = normalisedGraphQLErrors;
+    this.graphQLErrors = normalizedGraphQLErrors;
     this.networkError = networkError;
     this.response = response;
   }
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 bef93e2d..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"
@@ -4920,10 +4920,10 @@ react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6:
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
   integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==
 
-react-ssr-prepass@^1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/react-ssr-prepass/-/react-ssr-prepass-1.0.5.tgz#63ce763b2deb81949b20ccb2c64503ca53c562ba"
-  integrity sha512-A8QXXIHBpzr5/bnG3rwFgJWU6gLo7/dcW1bBIluoBI2hMyC+uuMRO+rXEtUpX6AVDhWfdFVi2I7kvWUDIRgDtA==
+react-ssr-prepass@^1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/react-ssr-prepass/-/react-ssr-prepass-1.0.6.tgz#5d4bcf210d7d59ea486f3567bd2df42c634a4dd7"
+  integrity sha512-UDY0nMboZ7kBM4IMQ8N0i8QlhY2s5oLgw0oZjA1znKPo6ep9U62pxPjSl1st0zMf2fwhb/GBw5snYqp/svuZSA==
   dependencies:
     object-is "^1.0.1"
 
@@ -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==
@@ -6247,10 +6255,10 @@ wide-align@^1.1.0:
   dependencies:
     string-width "^1.0.2 || 2"
 
-wonka@^3.1.1:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/wonka/-/wonka-3.1.1.tgz#c948e8299468dd87eb0d6fc9b5647f7848a85af2"
-  integrity sha512-UT65glpq1Bl9DaLwMJp6/EvsL+juqdkvJpAb2xsmUqmWLPkwIGB0/kIh8DHr3dem0UXHcgIDBizmH3xnnScguA==
+wonka@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/wonka/-/wonka-3.2.0.tgz#40b9ce8e0c375127437661290e98e5d9e5278440"
+  integrity sha512-sHCLouGzTclUlp1sdXAhuTqGMIoAQfpBit9puepaINPVqDBRtC8DPWtTUHC0DJIdFSO+DsO86AFBFCDor7Sn0g==
 
 wordwrap@~0.0.2:
   version "0.0.3"


Test patch:
diff --git a/src/__snapshots__/client.test.ts.snap b/src/__snapshots__/client.test.ts.snap
index 4885bab4..ec12ddf3 100644
--- a/src/__snapshots__/client.test.ts.snap
+++ b/src/__snapshots__/client.test.ts.snap
@@ -14,6 +14,7 @@ Client {
   "fetchOptions": undefined,
   "operations$": [Function],
   "reexecuteOperation": [Function],
+  "requestPolicy": "cache-first",
   "results$": [Function],
   "suspense": false,
   "url": "https://hostname.com",
diff --git a/src/__snapshots__/context.test.ts.snap b/src/__snapshots__/context.test.ts.snap
index 3a40f8a5..f223c558 100644
--- a/src/__snapshots__/context.test.ts.snap
+++ b/src/__snapshots__/context.test.ts.snap
@@ -27,6 +27,7 @@ Object {
       "fetchOptions": undefined,
       "operations$": [Function],
       "reexecuteOperation": [Function],
+      "requestPolicy": "cache-first",
       "results$": [Function],
       "suspense": false,
       "url": "/graphql",
@@ -44,6 +45,7 @@ Object {
       "fetchOptions": undefined,
       "operations$": [Function],
       "reexecuteOperation": [Function],
+      "requestPolicy": "cache-first",
       "results$": [Function],
       "suspense": false,
       "url": "/graphql",
@@ -80,6 +82,7 @@ Object {
       "fetchOptions": undefined,
       "operations$": [Function],
       "reexecuteOperation": [Function],
+      "requestPolicy": "cache-first",
       "results$": [Function],
       "suspense": false,
       "url": "/graphql",
@@ -97,6 +100,7 @@ Object {
       "fetchOptions": undefined,
       "operations$": [Function],
       "reexecuteOperation": [Function],
+      "requestPolicy": "cache-first",
       "results$": [Function],
       "suspense": false,
       "url": "/graphql",
diff --git a/src/client.test.ts b/src/client.test.ts
index 8d656d32..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';
 
@@ -50,7 +45,11 @@ beforeEach(() => {
   receivedOps = [];
   exchangeMock.mockClear();
   receiveMock.mockClear();
-  client = createClient({ url, exchanges: [exchangeMock] as any[] });
+  client = createClient({
+    url,
+    exchanges: [exchangeMock] as any[],
+    requestPolicy: 'cache-and-network',
+  });
 });
 
 describe('exchange args', () => {
@@ -85,6 +84,30 @@ describe('executeQuery', () => {
     expect(receivedOps[0]).toHaveProperty('variables', query.variables);
   });
 
+  it('passes requestPolicy to exchange', () => {
+    pipe(
+      client.executeQuery(query),
+      subscribe(x => x)
+    );
+
+    expect(receivedOps[0].context).toHaveProperty(
+      'requestPolicy',
+      'cache-and-network'
+    );
+  });
+
+  it('allows overriding the requestPolicy', () => {
+    pipe(
+      client.executeQuery(query, { requestPolicy: 'cache-first' }),
+      subscribe(x => x)
+    );
+
+    expect(receivedOps[0].context).toHaveProperty(
+      'requestPolicy',
+      'cache-first'
+    );
+  });
+
   it('passes operationName type to exchange', () => {
     pipe(
       client.executeQuery(query),
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);