Repository: FormidableLabs/urql
Pull request #816 (2020-05-18 08:52:05)

(next-urql) - streamline client creation
## Summary

This is a breaking change for `next-urql`, we were seeing a lot of confusion because of the `mergeExchanges` argument. This proposal streamlines it to the normal client.
 
However this does impose us to always specify a function for the creation of a client

Implements https://github.com/FormidableLabs/urql/issues/813

Still have to rewrite a lot of the testing.

TODO:
- [x] update examplees
- [x] update docs


changeset-bot[bot]: ###  💥  No Changeset

Latest commit: 39c63f249a674611a124deacfbc0988079936731

Merging this PR will not cause any packages to be released. If these changes should not cause updates to packages in this repo, this is fine 🙂

**If these changes should be published to npm, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/FormidableLabs/urql/new/feat/new-with-client?filename=.changeset/eleven-socks-march.md&value=---%0A%22next-urql%22%3A%20patch%0A---%0A%0A(next-urql)%20-%20streamline%20client%20creation%0A)


kitten: How does this look to you, @parkerziegler and @ryan-gilb? 😇

Patch:
diff --git a/.changeset/thick-avocados-enjoy.md b/.changeset/thick-avocados-enjoy.md
new file mode 100644
index 00000000..bc9db73b
--- /dev/null
+++ b/.changeset/thick-avocados-enjoy.md
@@ -0,0 +1,6 @@
+---
+'@urql/exchange-graphcache': minor
+---
+
+Issue warnings when an unknown type has been included in Graphcache's opts.key configuration to help spot typos.
+See: [#820](https://github.com/FormidableLabs/urql/pull/820)
diff --git a/.changeset/weak-pants-lick.md b/.changeset/weak-pants-lick.md
new file mode 100644
index 00000000..3f5b2bbb
--- /dev/null
+++ b/.changeset/weak-pants-lick.md
@@ -0,0 +1,26 @@
+---
+'next-urql': major
+---
+
+Transform the `withUrqlClient` function, this removes the second argument formerly called `mergeExchanges` and merges it with the first argument.
+The first argument will only accept functions from now on.
+
+To migrate you would transform from:
+
+```js
+export default withUrqlClient(
+  ctx => ({
+    url: '',
+  }),
+  ssrExchange => [exchanges]
+);
+```
+
+to
+
+```js
+export default withUrqlClient((ssrExchange, ctx) => ({
+  url: '',
+  exchanges: [exchanges],
+}));
+```
diff --git a/docs/advanced/server-side-rendering.md b/docs/advanced/server-side-rendering.md
index 35c7cd37..bbf103f8 100644
--- a/docs/advanced/server-side-rendering.md
+++ b/docs/advanced/server-side-rendering.md
@@ -213,7 +213,7 @@ const Index = () => {
   // ...
 };
 
-export default withUrqlClient(ctx => ({
+export default withUrqlClient((_ssrExchange, ctx) => ({
   // ...add your Client options here
   url: 'http://localhost:3000/graphql',
 }))(Index);
@@ -225,15 +225,15 @@ object or a function that receives the Next.js' `getInitialProps` context.
 
 One added caveat is that these options may not include the `exchanges` option because `next-urql`
 injects the `ssrExchange` automatically at the right location. If you're setting up custom exchanges
-you'll need to instead provide them in a custom `mergeExchanges` function as the second argument:
+you'll need to instead provide them in the `exchanges` property of the returned client object.
 
 ```js
 import { dedupExchange, cacheExchange, fetchExchange } from '@urql/core';
 
 import { withUrqlClient } from 'next-urql';
 
-// Modify this array to include custom exchanges:
-const mergeExchanges = ssrExchange => [dedupExchange, cacheExchange, ssrExchange, fetchExchange];
-
-export default withUrqlClient({ url: 'http://localhost:3000/graphql' }, mergeExchanges)(Index);
+export default withUrqlClient(ssrExchange => ({
+  url: 'http://localhost:3000/graphql',
+  exchanges: [dedupExchange, cacheExchange, ssrExchange, fetchExchange],
+}))(Index);
 ```
diff --git a/docs/basics/document-caching.md b/docs/basics/document-caching.md
index f7a8debc..42b5564c 100644
--- a/docs/basics/document-caching.md
+++ b/docs/basics/document-caching.md
@@ -86,7 +86,7 @@ to invalidate an additional entity.
 const [result, execute] = useMutation(`mutation($name: String!) { createUser(name: $name) }`);
 
 const onClick = () => {
-  execute({ context: { additionalTypenames: ['Wallet'] } });
+  execute({ name: 'newName' }, { additionalTypenames: ['Wallet'] });
 };
 ```
 
diff --git a/docs/graphcache/README.md b/docs/graphcache/README.md
index cf7d3e59..05d85ba2 100644
--- a/docs/graphcache/README.md
+++ b/docs/graphcache/README.md
@@ -47,6 +47,8 @@ default.
 - 📡 **Offline support** (work in progress) _Graphcache_ can persist and rehydrate its entire state,
   allowing an offline application to be built that is able to execute queries against the cache
   although the device is offline.
+- 🐛 [**Errors and warnings**](./errors.md). All potential errors are documented with information on
+  how you may be able to fix them.
 
 ## Installation and Setup
 
diff --git a/docs/graphcache/errors.md b/docs/graphcache/errors.md
new file mode 100644
index 00000000..9906965d
--- /dev/null
+++ b/docs/graphcache/errors.md
@@ -0,0 +1,291 @@
+---
+title: Errors
+order: 6
+---
+
+# Help!
+
+**This document lists out all errors and warnings in `@urql/exchange-graphcache`.**
+
+Any unexpected behaviour, condition, or error will be marked by an error or warning
+in development, which will output a helpful little message. Sometimes however, this
+message may not actually tell you everything about what's going on.
+
+This is a supporting document that explains every error and attempts to give more
+information on how you may be able to fix some issues or avoid these errors/warnings.
+
+## (1) Invalid GraphQL document
+
+> Invalid GraphQL document: All GraphQL documents must contain an OperationDefinition
+> node for a query, subscription, or mutation.
+
+There are multiple places where you're passing in GraphQL documents, either through
+methods on `Cache` (e.g. `cache.updateQuery`) or via `urql` using the `Client` or
+hooks like `useQuery`.
+
+Your queries must always contain a main operation, so either a query, mutation, or
+subscription. This error occurs when this is missing, because the `DocumentNode`
+is maybe empty or only contains fragments.
+
+## (2) Invalid Cache call
+
+> Invalid Cache call: The cache may only be accessed or mutated during
+> operations like write or query, or as part of its resolvers, updaters,
+> or optimistic configs.
+
+If you're somehow accessing the `Cache` (an instance of `Store`) outside of any
+of the usual operations then this error will be thrown.
+
+Please make sure that you're only calling methods on the `cache` as part of
+configs that you pass to your `cacheExchange`. Outside of these functions the cache
+must not be changed.
+
+However when you're not using the `cacheExchange` and are trying to use the
+`Store` on its own, then you may run into issues where its global state wasn't
+initialised correctly.
+
+This is a safe-guard to prevent any asynchronous work to take place, or to
+avoid mutating the cache outside of any normal operation.
+
+## (3) Invalid Object type
+
+> Invalid Object type: The type `???` is not an object in the defined schema,
+> but the GraphQL document is traversing it.
+
+When you're passing an introspected schema to the cache exchange, it is
+able to check whether all your queries are valid.
+This error occurs when an unknown type is found as part of a query or
+fragment.
+
+Check whether your schema is up-to-date or whether you're using an invalid
+typename somewhere, maybe due to a typo.
+
+## (4) Invalid field
+
+> Invalid field: The field `???` does not exist on `???`,
+> but the GraphQL document expects it to exist.<br />
+> Traversal will continue, however this may lead to undefined behavior!
+
+Similarly to the previous warning, when you're passing an introspected
+schema to the cache exchange, it is able to check whether all your queries are valid.
+This warning occurs when an unknown field is found on a selection set as part
+of a query or fragment.
+
+Check whether your schema is up-to-date or whether you're using an invalid
+field somewhere, maybe due to a typo.
+
+As the warning states, this won't lead any operation to abort or an error
+to be thrown!
+
+## (5) Invalid Abstract type
+
+> Invalid Abstract type: The type `???` is not an Interface or Union type
+> in the defined schema, but a fragment in the GraphQL document is using it
+> as a type condition.
+
+When you're passing an introspected schema to the cache exchange, it becomes
+able to deterministically check whether an entity in the cache matches a fragment's
+type condition.
+
+This applies to full fragments (`fragment _ on Interface`) or inline fragments
+(`... on Interface`), that apply to interfaces instead of to a concrete object typename.
+
+Check whether your schema is up-to-date or whether you're using an invalid
+field somewhere, maybe due to a typo.
+
+## (6) readFragment(...) was called with an empty fragment
+
+> readFragment(...) was called with an empty fragment.
+> You have to call it with at least one fragment in your GraphQL document.
+
+You probably have called `cache.readFragment` with a GraphQL
+document that doesn't contain a main fragment.
+
+This error occurs when no main fragment can be found, because the `DocumentNode`
+is maybe empty or does not contain fragments.
+
+When you're calling a fragment method, please ensure that you're only passing fragments
+in your GraphQL document. The first fragment will be used to start writing data.
+
+## (7) Can't generate a key for readFragment(...)
+
+> Can't generate a key for readFragment(...).
+> You have to pass an `id` or `_id` field or create a custom `keys` config for `???`.
+
+You probably have called `cache.readFragment` with data that the cache can't generate a
+key for.
+
+This may either happen because you're missing the `id` or `_id` field or some other
+fields for your custom `keys` config.
+
+Please make sure that you include enough properties on your data so that `readFragment`
+can generate a key.
+
+## (8) Invalid resolver data
+
+> Invalid resolver value: The resolver at `???` returned an invalid typename that
+> could not be reconciled with the cache.
+
+This error may occur when you provide a cache resolver for a field using `resolvers` config.
+
+The value that you returns needs to contain a `__typename` field and this field must
+match the `__typename` field that exists in the cache, if any. This is because it's not
+possible to return a different type for a single field.
+
+Please check your schema for the type that your resolver has to return, then add a
+`__typename` field to your returned resolver value that matches this type.
+
+## (9) Invalid resolver value
+
+> Invalid resolver value: The field at `???` is a scalar (number, boolean, etc),
+> but the GraphQL query expects a selection set for this field.
+
+The GraphQL query that has been walked contains a selection set at the place where
+your resolver is located.
+
+This means that a full entity object needs to be returned, but instead the cache
+received a number, boolean, or another scalar from your resolver.
+
+Please check that your resolvers return scalars where there's no selection set,
+and entities where there is one.
+
+## (10) writeOptimistic(...) was called with an operation that isn't a mutation
+
+> writeOptimistic(...) was called with an operation that is not a mutation.
+> This case is unsupported and should never occur.
+
+This should never happen, please open an issue if it does. This occurs when `writeOptimistic`
+attempts to write an optimistic result for a query or subscription, instead of a mutation.
+
+## (11) writeFragment(...) was called with an empty fragment
+
+> writeFragment(...) was called with an empty fragment.
+> You have to call it with at least one fragment in your GraphQL document.
+
+You probably have called `cache.writeFragment` with a GraphQL
+document that doesn't contain a main fragment.
+
+This error occurs when no main fragment can be found, because the `DocumentNode`
+is maybe empty or does not contain fragments.
+
+When you're calling a fragment method, please ensure that you're only passing fragments
+in your GraphQL document. The first fragment will be used to start writing data.
+
+## (12) Can't generate a key for writeFragment(...)
+
+> Can't generate a key for writeFragment(...) data.
+> You have to pass an `id` or `_id` field or create a custom `keys` config for `???`.
+
+You probably have called `cache.writeFragment` with data that the cache can't generate a
+key for.
+
+This may either happen because you're missing the `id` or `_id` field or some other
+fields for your custom `keys` config.
+
+Please make sure that you include enough properties on your data so that `writeFragment`
+can generate a key.
+
+## (13) Invalid undefined
+
+> Invalid undefined: The field at `???` is `undefined`, but the GraphQL query expects a
+> scalar (number, boolean, etc) / selection set for this field.
+
+As data is written to the cache, this warning is issued when `undefined` is encountered.
+GraphQL results should never contain an `undefined` value, so this warning will let you
+know which part of your result did contain `undefined`.
+
+## (15) Invalid key
+
+> Invalid key: The GraphQL query at the field at `???` has a selection set,
+> but no key could be generated for the data at this field.
+> You have to request `id` or `_id` fields for all selection sets or create a
+> custom `keys` config for `???`.
+> Entities without keys will be embedded directly on the parent entity.
+> If this is intentional, create a `keys` config for `???` that always returns null.
+
+This error occurs when the cache can't generate a key for an entity. The key
+would then effectively be `null` and the entity won't be cached by a key.
+
+Conceptually this means that an entity won't be normalised but will indeed
+be cached by the parent's key and field, which is displayed in the first
+part of the warning.
+
+This may mean that you forgot to include an `id` or `_id` field.
+
+But if your entity at that place doesn't have any `id` fields, then you may
+have to create a custom `keys` config. This `keys` function either needs to
+return a unique ID for your entity or it needs to explicitly return `null` to silence
+this warning.
+
+## (16) Heuristic Fragment Matching
+
+> Heuristic Fragment Matching: A fragment is trying to match against the `???` type,
+> but the type condition is `???`. Since GraphQL allows for interfaces `???` may be
+> an interface.
+> A schema needs to be defined for this match to be deterministic, otherwise
+> the fragment will be matched heuristically!
+
+This warning is issued on fragment matching. Fragment matching is the process
+of matching a fragment against a piece of data in the cache and that data's `__typename`
+field.
+
+When the `__typename` field doesn't match the fragment's type, then we may be
+dealing with an interface and/or enum. In such a case the fragment may _still match_
+if it's referring to an interface (`... on Interface`). Graphcache is supposed to be
+usable without much config, so what it does in this case is apply a heuristic match.
+
+In a heuristic fragment match we check whether all fields on the fragment are present
+in the cache, which is then treated as a fragment match.
+
+When you pass an introspected schema to the cache, this warning will never be displayed
+as the cache can then do deterministic fragment matching using schema information.
+
+## (17) Invalid type
+
+> Invalid type: The type `???` is used with @populate but does not exist.
+
+When you're using the populate exchange with an introspected schema and add the
+`@populate` directive to fields it first checks whether the type is valid and
+exists on the schema.
+
+If the field does not have sufficient type information because it doesn't exist
+on the schema or does not match expectations then this warning is logged.
+
+Check whether your schema is up-to-date or whether you're using an invalid
+field somewhere, maybe due to a typo.
+
+## (18) Invalid TypeInfo state
+
+> Invalid TypeInfo state: Found no flat schema type when one was expected.
+
+When you're using the populate exchange with an introspected schema, it will
+start collecting used fragments and selection sets on all of your queries.
+This error may occur if it hits unexpected types or inexistent types when doing so.
+
+Check whether your schema is up-to-date or whether you're using an invalid
+field somewhere, maybe due to a typo.
+
+Please open an issue if it happens on a query that you expect to be supported
+by the `populateExchange`.
+
+## (19) Can't generate a key for invalidate(...)
+
+> Can't generate a key for invalidate(...).
+> You need to pass in a valid key (**typename:id) or an object with the "**typename" property and an "id" or "\_id" property.
+
+You probably have called `cache.invalidate` with data that the cache can't generate a key for.
+
+This may either happen because you're missing the `__typename` and `id` or `_id` field or if the last two
+aren't applicable to this entity a custom `keys` entry.
+
+## (20) Invalid Object type
+
+> Invalid Object type: The type `???` is not an object in the defined schema,
+> but the `keys` option is referencing it.
+
+When you're passing an introspected schema to the cache exchange, it is
+able to check whether your `opts.keys` is valid.
+This error occurs when an unknown type is found in `opts.keys`.
+
+Check whether your schema is up-to-date, or whether you're using an invalid
+typename in `opts.keys`, maybe due to a typo.
diff --git a/docs/graphcache/schema-awareness.md b/docs/graphcache/schema-awareness.md
index 3d534648..df445d30 100644
--- a/docs/graphcache/schema-awareness.md
+++ b/docs/graphcache/schema-awareness.md
@@ -6,10 +6,10 @@ order: 4
 # Schema Awareness
 
 As mentioned in the docs we allow for the schema to be passed
-to the `cacheExchange` this allows for partial results and deterministic
+to the `cacheExchange`. This allows for partial results and deterministic
 fragment matching.
 With deterministic fragment matching we mean that if you use an interface
-or a union we will be 100% sure you're allowed to do so, we'll check if the
+or a union we will be 100% sure you're allowed to do so, therefore we'll check if the
 type you request can actually be returned from this union/interface.
 
 ## Getting your schema
diff --git a/exchanges/graphcache/help.md b/exchanges/graphcache/help.md
index c038b65a..021913fd 100644
--- a/exchanges/graphcache/help.md
+++ b/exchanges/graphcache/help.md
@@ -1,274 +1,7 @@
-# Help!
+This file (`exchanges/graphcache/help.md`) has been moved to `docs/graphcache/errors.md`
 
-**This document lists out all errors and warnings in `@urql/exchange-graphcache`.**
-
-Any unexpected behaviour, condition, or error will be marked by an error or warning
-in development, which will output a helpful little message. Sometimes however, this
-message may not actually tell you everything about what's going on.
-
-This is a supporting document that explains every error and attempts to give more
-information on how you may be able to fix some issues or avoid these errors/warnings.
-
-## (1) Invalid GraphQL document <a id="1"></a>
-
-> Invalid GraphQL document: All GraphQL documents must contain an OperationDefinition
-> node for a query, subscription, or mutation.
-
-There are multiple places where you're passing in GraphQL documents, either through
-methods on `Cache` (e.g. `cache.updateQuery`) or via `urql` using the `Client` or
-hooks like `useQuery`.
-
-Your queries must always contain a main operation, so either a query, mutation, or
-subscription. This error occurs when this is missing, because the `DocumentNode`
-is maybe empty or only contains fragments.
-
-## (2) Invalid Cache call <a id="2"></a>
-
-> Invalid Cache call: The cache may only be accessed or mutated during
-> operations like write or query, or as part of its resolvers, updaters,
-> or optimistic configs.
-
-If you're somehow accessing the `Cache` (an instance of `Store`) outside of any
-of the usual operations then this error will be thrown.
-
-Please make sure that you're only calling methods on the `cache` as part of
-configs that you pass to your `cacheExchange`. Outside of these functions the cache
-must not be changed.
-
-However when you're not using the `cacheExchange` and are trying to use the
-`Store` on its own, then you may run into issues where its global state wasn't
-initialised correctly.
-
-This is a safe-guard to prevent any asynchronous work to take place, or to
-avoid mutating the cache outside of any normal operation.
-
-## (3) Invalid Object type <a id="3"></a>
-
-> Invalid Object type: The type `???` is not an object in the defined schema,
-> but the GraphQL document is traversing it.
-
-When you're passing an introspected schema to the cache exchange, it is
-able to check whether all your queries are valid.
-This error occurs when an unknown type is found as part of a query or
-fragment.
-
-Check whether your schema is up-to-date or whether you're using an invalid
-typename somewhere, maybe due to a typo.
-
-## (4) Invalid field <a id="4"></a>
-
-> Invalid field: The field `???` does not exist on `???`,
-> but the GraphQL document expects it to exist.<br />
-> Traversal will continue, however this may lead to undefined behavior!
-
-Similarly to the previous warning, when you're passing an introspected
-schema to the cache exchange, it is able to check whether all your queries are valid.
-This warning occurs when an unknown field is found on a selection set as part
-of a query or fragment.
-
-Check whether your schema is up-to-date or whether you're using an invalid
-field somewhere, maybe due to a typo.
-
-As the warning states, this won't lead any operation to abort or an error
-to be thrown!
-
-## (5) Invalid Abstract type <a id="5"></a>
-
-> Invalid Abstract type: The type `???` is not an Interface or Union type
-> in the defined schema, but a fragment in the GraphQL document is using it
-> as a type condition.
-
-When you're passing an introspected schema to the cache exchange, it becomes
-able to deterministically check whether an entity in the cache matches a fragment's
-type condition.
-
-This applies to full fragments (`fragment _ on Interface`) or inline fragments
-(`... on Interface`), that apply to interfaces instead of to a concrete object typename.
-
-Check whether your schema is up-to-date or whether you're using an invalid
-field somewhere, maybe due to a typo.
-
-## (6) readFragment(...) was called with an empty fragment <a id="6"></a>
-
-> readFragment(...) was called with an empty fragment.
-> You have to call it with at least one fragment in your GraphQL document.
-
-You probably have called `cache.readFragment` with a GraphQL
-document that doesn't contain a main fragment.
-
-This error occurs when no main fragment can be found, because the `DocumentNode`
-is maybe empty or does not contain fragments.
-
-When you're calling a fragment method, please ensure that you're only passing fragments
-in your GraphQL document. The first fragment will be used to start writing data.
-
-## (7) Can't generate a key for readFragment(...) <a id="7"></a>
-
-> Can't generate a key for readFragment(...).
-> You have to pass an `id` or `_id` field or create a custom `keys` config for `???`.
-
-You probably have called `cache.readFragment` with data that the cache can't generate a
-key for.
-
-This may either happen because you're missing the `id` or `_id` field or some other
-fields for your custom `keys` config.
-
-Please make sure that you include enough properties on your data so that `readFragment`
-can generate a key.
-
-## (8) Invalid resolver data <a id="8"></a>
-
-> Invalid resolver value: The resolver at `???` returned an invalid typename that
-> could not be reconciled with the cache.
-
-This error may occur when you provide a cache resolver for a field using `resolvers` config.
-
-The value that you returns needs to contain a `__typename` field and this field must
-match the `__typename` field that exists in the cache, if any. This is because it's not
-possible to return a different type for a single field.
-
-Please check your schema for the type that your resolver has to return, then add a
-`__typename` field to your returned resolver value that matches this type.
-
-## (9) Invalid resolver value <a id="9"></a>
-
-> Invalid resolver value: The field at `???` is a scalar (number, boolean, etc),
-> but the GraphQL query expects a selection set for this field.
-
-The GraphQL query that has been walked contains a selection set at the place where
-your resolver is located.
-
-This means that a full entity object needs to be returned, but instead the cache
-received a number, boolean, or another scalar from your resolver.
-
-Please check that your resolvers return scalars where there's no selection set,
-and entities where there is one.
-
-## (10) writeOptimistic(...) was called with an operation that isn't a mutation <a id="10"></a>
-
-> writeOptimistic(...) was called with an operation that is not a mutation.
-> This case is unsupported and should never occur.
-
-This should never happen, please open an issue if it does. This occurs when `writeOptimistic`
-attempts to write an optimistic result for a query or subscription, instead of a mutation.
-
-## (11) writeFragment(...) was called with an empty fragment <a id="11"></a>
-
-> writeFragment(...) was called with an empty fragment.
-> You have to call it with at least one fragment in your GraphQL document.
-
-You probably have called `cache.writeFragment` with a GraphQL
-document that doesn't contain a main fragment.
-
-This error occurs when no main fragment can be found, because the `DocumentNode`
-is maybe empty or does not contain fragments.
-
-When you're calling a fragment method, please ensure that you're only passing fragments
-in your GraphQL document. The first fragment will be used to start writing data.
-
-## (12) Can't generate a key for writeFragment(...) <a id="12"></a>
-
-> Can't generate a key for writeFragment(...) data.
-> You have to pass an `id` or `_id` field or create a custom `keys` config for `???`.
-
-You probably have called `cache.writeFragment` with data that the cache can't generate a
-key for.
-
-This may either happen because you're missing the `id` or `_id` field or some other
-fields for your custom `keys` config.
-
-Please make sure that you include enough properties on your data so that `writeFragment`
-can generate a key.
-
-## (13) Invalid undefined <a id="13"></a>
-
-> Invalid undefined: The field at `???` is `undefined`, but the GraphQL query expects a
-> scalar (number, boolean, etc) / selection set for this field.
-
-As data is written to the cache, this warning is issued when `undefined` is encountered.
-GraphQL results should never contain an `undefined` value, so this warning will let you
-know which part of your result did contain `undefined`.
-
-## (15) Invalid key <a id="15"></a>
-
-> Invalid key: The GraphQL query at the field at `???` has a selection set,
-> but no key could be generated for the data at this field.
-> You have to request `id` or `_id` fields for all selection sets or create a
-> custom `keys` config for `???`.
-> Entities without keys will be embedded directly on the parent entity.
-> If this is intentional, create a `keys` config for `???` that always returns null.
-
-This error occurs when the cache can't generate a key for an entity. The key
-would then effectively be `null` and the entity won't be cached by a key.
-
-Conceptually this means that an entity won't be normalised but will indeed
-be cached by the parent's key and field, which is displayed in the first
-part of the warning.
-
-This may mean that you forgot to include an `id` or `_id` field.
-
-But if your entity at that place doesn't have any `id` fields, then you may
-have to create a custom `keys` config. This `keys` function either needs to
-return a unique ID for your entity or it needs to explicitly return `null` to silence
-this warning.
-
-## (16) Heuristic Fragment Matching <a id="16"></a>
-
-> Heuristic Fragment Matching: A fragment is trying to match against the `???` type,
-> but the type condition is `???`. Since GraphQL allows for interfaces `???` may be
-> an interface.
-> A schema needs to be defined for this match to be deterministic, otherwise
-> the fragment will be matched heuristically!
-
-This warning is issued on fragment matching. Fragment matching is the process
-of matching a fragment against a piece of data in the cache and that data's `__typename`
-field.
-
-When the `__typename` field doesn't match the fragment's type, then we may be
-dealing with an interface and/or enum. In such a case the fragment may _still match_
-if it's referring to an interface (`... on Interface`). Graphcache is supposed to be
-usable without much config, so what it does in this case is apply a heuristic match.
-
-In a heuristic fragment match we check whether all fields on the fragment are present
-in the cache, which is then treated as a fragment match.
-
-When you pass an introspected schema to the cache, this warning will never be displayed
-as the cache can then do deterministic fragment matching using schema information.
-
-## (17) Invalid type <a id="17"></a>
-
-> Invalid type: The type `???` is used with @populate but does not exist.
-
-When you're using the populate exchange with an introspected schema and add the
-`@populate` directive to fields it first checks whether the type is valid and
-exists on the schema.
-
-If the field does not have sufficient type information because it doesn't exist
-on the schema or does not match expectations then this warning is logged.
-
-Check whether your schema is up-to-date or whether you're using an invalid
-field somewhere, maybe due to a typo.
-
-## (18) Invalid TypeInfo state <a id="18"></a>
-
-> Invalid TypeInfo state: Found no flat schema type when one was expected.
-
-When you're using the populate exchange with an introspected schema, it will
-start collecting used fragments and selection sets on all of your queries.
-This error may occur if it hits unexpected types or inexistent types when doing so.
-
-Check whether your schema is up-to-date or whether you're using an invalid
-field somewhere, maybe due to a typo.
-
-Please open an issue if it happens on a query that you expect to be supported
-by the `populateExchange`.
-
-## (19) Can't generate a key for invalidate(...)
-
-> Can't generate a key for invalidate(...).
-> You need to pass in a valid key (**typename:id) or an object with the "**typename" property and an "id" or "\_id" property.
-
-You probably have called `cache.invalidate` with data that the cache can't generate a key for.
-
-This may either happen because you're missing the `__typename` and `id` or `_id` field or if the last two
-aren't applicable to this entity a custom `keys` entry.
+If you are looking at this in a browser
+* ...and your URL looks like this: `github.com/FormidableLabs/urql/blob/master/exchanges/graphcache/help.md#15`
+* ...in the URL, replace `exchanges/graphcache/help.md` with `docs/graphcache/errors.md`
+* ...and keep the `#15`
+* ...and then you will get help with your error!
diff --git a/exchanges/graphcache/src/ast/schemaPredicates.ts b/exchanges/graphcache/src/ast/schemaPredicates.ts
index 6e0f2333..6fe83c23 100644
--- a/exchanges/graphcache/src/ast/schemaPredicates.ts
+++ b/exchanges/graphcache/src/ast/schemaPredicates.ts
@@ -10,6 +10,7 @@ import {
 } from 'graphql';
 
 import { warn, invariant } from '../helpers/help';
+import { KeyingConfig } from '../types';
 
 export const isFieldNullable = (
   schema: GraphQLSchema,
@@ -111,3 +112,22 @@ function expectAbstractType(
     5
   );
 }
+
+export function expectValidKeyingConfig(
+  schema: GraphQLSchema,
+  keys: KeyingConfig
+): void {
+  if (process.env.NODE_ENV !== 'production') {
+    const types = Object.keys(schema.getTypeMap());
+    Object.keys(keys).forEach(key => {
+      if (types.indexOf(key) === -1) {
+        warn(
+          'Invalid Object type: The type `' +
+            key +
+            '` is not an object in the defined schema, but the `keys` option is referencing it.',
+          20
+        );
+      }
+    });
+  }
+}
diff --git a/exchanges/graphcache/src/helpers/help.ts b/exchanges/graphcache/src/helpers/help.ts
index 39a60040..ff4c7634 100644
--- a/exchanges/graphcache/src/helpers/help.ts
+++ b/exchanges/graphcache/src/helpers/help.ts
@@ -1,7 +1,7 @@
 // These are guards that are used throughout the codebase to warn or error on
 // unexpected behaviour or conditions.
 // Every warning and error comes with a number that uniquely identifies them.
-// You can read more about the messages themselves in `docs/help.md`
+// You can read more about the messages themselves in `docs/graphcache/errors.md`
 
 import { Kind, ExecutableDefinitionNode, InlineFragmentNode } from 'graphql';
 
@@ -23,11 +23,13 @@ export type ErrorCode =
   | 16
   | 17
   | 18
-  | 19;
+  | 19
+  | 20;
 
 type DebugNode = ExecutableDefinitionNode | InlineFragmentNode;
 
-const helpUrl = '\nhttps://bit.ly/38yWDau#';
+// URL unfurls to https://formidable.com/open-source/urql/docs/graphcache/errors/
+const helpUrl = '\nhttps://bit.ly/2XbVrpR#';
 const cache = new Set<string>();
 
 export const currentDebugStack: string[] = [];
diff --git a/exchanges/graphcache/src/store/store.ts b/exchanges/graphcache/src/store/store.ts
index db5e689f..c8fd2942 100644
--- a/exchanges/graphcache/src/store/store.ts
+++ b/exchanges/graphcache/src/store/store.ts
@@ -27,6 +27,7 @@ import { writeFragment, startWrite } from '../operations/write';
 import { invalidate, invalidateEntity } from '../operations/invalidate';
 import { keyOfField } from './keys';
 import * as InMemoryData from './data';
+import * as SchemaPredicates from '../ast/schemaPredicates';
 
 type RootField = 'query' | 'mutation' | 'subscription';
 
@@ -73,6 +74,10 @@ export class Store implements Cache {
       if (queryType) queryName = queryType.name;
       if (mutationType) mutationName = mutationType.name;
       if (subscriptionType) subscriptionName = subscriptionType.name;
+
+      if (this.keys) {
+        SchemaPredicates.expectValidKeyingConfig(this.schema, this.keys);
+      }
     }
 
     this.rootFields = {
diff --git a/exchanges/populate/src/helpers/help.ts b/exchanges/populate/src/helpers/help.ts
index bdd56507..41372ce2 100644
--- a/exchanges/populate/src/helpers/help.ts
+++ b/exchanges/populate/src/helpers/help.ts
@@ -1,7 +1,7 @@
 // These are guards that are used throughout the codebase to warn or error on
 // unexpected behaviour or conditions.
 // Every warning and error comes with a number that uniquely identifies them.
-// You can read more about the messages themselves in `docs/help.md`
+// You can read more about the messages themselves in `docs/graphcache/errors.md`
 export type ErrorCode =
   | 1
   | 2
@@ -22,7 +22,8 @@ export type ErrorCode =
   | 18
   | 19;
 
-const helpUrl = '\nhttps://bit.ly/38yWDau#';
+// URL unfurls to https://formidable.com/open-source/urql/docs/graphcache/errors/
+const helpUrl = '\nhttps://bit.ly/2XbVrpR#';
 const cache = new Set<string>();
 
 export function invariant(
diff --git a/packages/next-urql/README.md b/packages/next-urql/README.md
index 9eaa4a82..3761e1e4 100644
--- a/packages/next-urql/README.md
+++ b/packages/next-urql/README.md
@@ -60,7 +60,7 @@ const Root = () => (
   </div>
 );
 
-export default withUrqlClient({ url: 'https://graphql-pokemon.now.sh' })(Root);
+export default withUrqlClient(() => ({ url: 'https://graphql-pokemon.now.sh' }))(Root);
 ```
 
 Read more below in the [API](#API) section to learn more about the arguments that can be passed to `withUrqlClient`.
@@ -78,9 +78,7 @@ Next allows you to override the root of your application using a special page ca
 The `clientOptions` argument is required. It represents all of the options you want to enable on your `urql` Client instance. It has the following union type:
 
 ```typescript
-type NextUrqlClientConfig =
-  | Omit<ClientOptions, 'exchanges' | 'suspense'>
-  | ((ctx: NextPageContext) => Omit<ClientOptions, 'exchanges' | 'suspense'>);
+type NextUrqlClientConfig = (ssrExchange: SSRExchange, ctx?: NextPageContext) => ClientOptions;
 ```
 
 The `ClientOptions` `interface` comes from `urql` itself and has the following type:
@@ -93,7 +91,7 @@ interface ClientOptions {
   fetchOptions?: RequestInit | (() => RequestInit);
   /** An alternative fetch implementation. */
   fetch?: typeof fetch;
-  /** The exchanges used by the Client. See mergeExchanges below for information on modifying exchanges in next-urql. */
+  /** The exchanges used by the Client. */
   exchanges?: Exchange[];
   /** A flag to enable suspense on the server. next-urql handles this for you. */
   suspense?: boolean;
@@ -106,22 +104,10 @@ interface ClientOptions {
 }
 ```
 
-This means you have two options for creating your `urql` Client. The first involves just passing the options as an object directly:
+You can create a client by passing a function which receives the `ssrExchange` and Next's context object, `ctx`, as arguments and returns `urql`'s Client options. This is helpful if you need to access some part of Next's context to instantiate your Client options. **Note: `ctx` is _only_ available on the initial server-side render and _not_ on client-side navigation**. This is necessary to allow for different Client configurations between server and client.
 
 ```typescript
-withUrqlClient({
-  url: 'http://localhost:3000',
-  fetchOptions: {
-    referrer: 'no-referrer',
-    redirect: 'follow',
-  },
-});
-```
-
-The second involves passing a function, which receives Next's context object, `ctx`, as an argument and returns `urql`'s Client options. This is helpful if you need to access some part of Next's context to instantiate your Client options. **Note: `ctx` is _only_ available on the initial server-side render and _not_ on client-side navigation**. This is necessary to allow for different Client configurations between server and client.
-
-```typescript
-withUrqlClient(ctx => ({
+withUrqlClient((_ssrExchange, ctx) => ({
   url: 'http://localhost:3000',
   fetchOptions: {
     headers: {
@@ -135,15 +121,11 @@ withUrqlClient(ctx => ({
 
 In client-side SPAs using `urql`, you typically configure the Client yourself and pass it as the `value` prop to `urql`'s context `Provider`. `withUrqlClient` handles setting all of this up for you under the hood. By default, you'll be opted into server-side `Suspense` and have the necessary `exchanges` set up for you, including the [`ssrExchange`](https://formidable.com/open-source/urql/docs/api/#ssrexchange-exchange-factory). If you need to customize your exchanges beyond the defaults `next-urql` provides, use the second argument to `withUrqlClient`, `mergeExchanges`.
 
-#### `mergeExchanges` (Optional)
+#### `exchanges`
 
-The `mergeExchanges` argument is optional. This is a function that takes the `ssrExchange` created by `next-urql` as its only argument and allows you to configure your exchanges as you wish. It has the following type signature:
-
-```typescript
-(ssrExchange: SSRExchange) => Exchange[]
-```
+When you're using `withUrqlClient` and you don't return an `exchanges` property we'll assume you wanted the default exchanges, these contain: `dedupExchange`, `cacheExchange`, `ssrExchange` (the one you received as a first argument) and the `fetchExchange`.
 
-By default, `next-urql` will incorprate the `ssrExchange` into your `exchanges` array in the correct location (after any other caching exchanges, but _before_ the `fetchExchange` – read more [here](https://formidable.com/open-source/urql/docs/basics/#setting-up-the-client)). Use this argument if you want to configure your Client with additional custom `exchanges`, or access the `ssrCache` directly to extract or restore data from its cache.
+When you yourself want to pass exchanges don't forget to include the `ssrExchange` you received as the first argument.
 
 ### Different Client configurations on the client and the server
 
diff --git a/packages/next-urql/examples/1-with-urql-client/pages/index.tsx b/packages/next-urql/examples/1-with-urql-client/pages/index.tsx
index 6dce1be7..45982b54 100644
--- a/packages/next-urql/examples/1-with-urql-client/pages/index.tsx
+++ b/packages/next-urql/examples/1-with-urql-client/pages/index.tsx
@@ -30,7 +30,7 @@ Home.getInitialProps = () => {
   };
 };
 
-export default withUrqlClient((ctx: NextUrqlPageContext) => {
+export default withUrqlClient((_ssr: object, ctx: NextUrqlPageContext) => {
   return {
     url: 'https://graphql-pokemon.now.sh',
     fetchOptions: {
diff --git a/packages/next-urql/examples/2-with-_app.js/pages/_app.tsx b/packages/next-urql/examples/2-with-_app.js/pages/_app.tsx
index b0aa8de7..cebf96bb 100644
--- a/packages/next-urql/examples/2-with-_app.js/pages/_app.tsx
+++ b/packages/next-urql/examples/2-with-_app.js/pages/_app.tsx
@@ -17,7 +17,7 @@ App.getInitialProps = async (ctx: NextUrqlAppContext) => {
   };
 };
 
-export default withUrqlClient({ url: 'https://graphql-pokemon.now.sh', fetch })(
+export default withUrqlClient(() => ({ url: 'https://graphql-pokemon.now.sh', fetch }))(
   // @ts-ignore
   App
 );
diff --git a/packages/next-urql/examples/3-with-custom-exchange/pages/index.tsx b/packages/next-urql/examples/3-with-custom-exchange/pages/index.tsx
index dae8c55c..737cc355 100644
--- a/packages/next-urql/examples/3-with-custom-exchange/pages/index.tsx
+++ b/packages/next-urql/examples/3-with-custom-exchange/pages/index.tsx
@@ -18,13 +18,14 @@ const Home: React.FC = () => (
   </div>
 );
 
-export default withUrqlClient(
-  { url: 'https://graphql-pokemon.now.sh', fetch },
-  (ssrExchange: SSRExchange) => [
+export default withUrqlClient((ssrExchange) => ({
+  exchanges: [
     dedupExchange,
     urlExchange,
     cacheExchange,
     ssrExchange,
     fetchExchange,
-  ]
-)(Home);
+  ],
+  url: 'https://graphql-pokemon.now.sh',
+  fetch,
+}))(Home);
diff --git a/packages/next-urql/examples/4-with-navigation/pages/[pokemon].tsx b/packages/next-urql/examples/4-with-navigation/pages/[pokemon].tsx
index fe92d656..0806bdb2 100644
--- a/packages/next-urql/examples/4-with-navigation/pages/[pokemon].tsx
+++ b/packages/next-urql/examples/4-with-navigation/pages/[pokemon].tsx
@@ -49,7 +49,7 @@ const Pokemon = () => {
   return <h1>{result.data.pokemon.name}</h1>;
 };
 
-export default withUrqlClient({
+export default withUrqlClient(() => ({
   url: 'https://graphql-pokemon.now.sh',
   fetch,
-})(Pokemon);
+}))(Pokemon);
diff --git a/packages/next-urql/examples/4-with-navigation/pages/index.tsx b/packages/next-urql/examples/4-with-navigation/pages/index.tsx
index b411f79a..c9ca8f83 100644
--- a/packages/next-urql/examples/4-with-navigation/pages/index.tsx
+++ b/packages/next-urql/examples/4-with-navigation/pages/index.tsx
@@ -15,7 +15,7 @@ const Home: NextComponentType<NextUrqlPageContext> = () => (
   </div>
 );
 
-export default withUrqlClient((ctx: NextUrqlPageContext) => {
+export default withUrqlClient((_ssr: object, ctx: NextUrqlPageContext) => {
   return {
     url: 'https://graphql-pokemon.now.sh',
     fetchOptions: {
diff --git a/packages/next-urql/examples/5-with-suspense-exchange/pages/index.tsx b/packages/next-urql/examples/5-with-suspense-exchange/pages/index.tsx
index a8db17fc..8b556bcb 100644
--- a/packages/next-urql/examples/5-with-suspense-exchange/pages/index.tsx
+++ b/packages/next-urql/examples/5-with-suspense-exchange/pages/index.tsx
@@ -18,13 +18,15 @@ const Home: React.FC = () => (
   </div>
 );
 
-export default withUrqlClient(
-  { url: 'https://graphql-pokemon.now.sh', fetch, suspense: true },
-  (ssrExchange: SSRExchange) => [
+export default withUrqlClient((ssrExchange) => ({
+  url: 'https://graphql-pokemon.now.sh',
+  fetch,
+  suspense: true,
+  exchanges: [
     dedupExchange,
     suspenseExchange,
     cacheExchange,
     ssrExchange,
     fetchExchange,
   ]
-)(Home);
+}))(Home);
diff --git a/packages/next-urql/src/init-urql-client.ts b/packages/next-urql/src/init-urql-client.ts
index 0cff932c..f185f576 100644
--- a/packages/next-urql/src/init-urql-client.ts
+++ b/packages/next-urql/src/init-urql-client.ts
@@ -1,42 +1,20 @@
-import {
-  createClient,
-  dedupExchange,
-  cacheExchange,
-  fetchExchange,
-  ssrExchange,
-  Client,
-  Exchange,
-} from 'urql';
+import { createClient, Client, ClientOptions } from 'urql';
 import 'isomorphic-unfetch';
-import { NextUrqlClientOptions, SSRData, SSRExchange } from './types';
 
 let urqlClient: Client | null = null;
-let ssrCache: SSRExchange | null = null;
 
-export function initUrqlClient(
-  clientOptions: NextUrqlClientOptions,
-  mergeExchanges: (ssrEx: SSRExchange) => Exchange[] = ssrEx => [
-    dedupExchange,
-    cacheExchange,
-    ssrEx,
-    fetchExchange,
-  ],
-  initialState?: SSRData
-): [Client | null, SSRExchange | null] {
+export function initUrqlClient(clientOptions: ClientOptions): Client | null {
   // Create a new Client for every server-side rendered request.
   // This ensures we reset the state for each rendered page.
   // If there is an exising client instance on the client-side, use it.
   const isServer = typeof window === 'undefined';
   if (isServer || !urqlClient) {
-    ssrCache = ssrExchange({ initialState });
-
     urqlClient = createClient({
       ...clientOptions,
       suspense: isServer || clientOptions.suspense,
-      exchanges: mergeExchanges(ssrCache),
     });
   }
 
   // Return both the Client instance and the ssrCache.
-  return [urqlClient, ssrCache];
+  return urqlClient;
 }
diff --git a/packages/next-urql/src/types.ts b/packages/next-urql/src/types.ts
index 7c874f51..d693d178 100644
--- a/packages/next-urql/src/types.ts
+++ b/packages/next-urql/src/types.ts
@@ -3,13 +3,10 @@ import { NextPageContext } from 'next';
 import { ClientOptions, Exchange, Client } from 'urql';
 import { AppContext } from 'next/app';
 
-export type NextUrqlClientOptions = Omit<ClientOptions, 'exchanges'>;
-
-export type NextUrqlClientConfig =
-  | NextUrqlClientOptions
-  | ((ctx?: NextPageContext) => NextUrqlClientOptions);
-
-export type MergeExchanges = (ssrExchange: SSRExchange) => Exchange[];
+export type NextUrqlClientConfig = (
+  ssrExchange: SSRExchange,
+  ctx?: NextPageContext
+) => ClientOptions;
 
 export interface NextUrqlPageContext extends NextPageContext {
   urqlClient: Client;
diff --git a/packages/next-urql/src/with-urql-client.tsx b/packages/next-urql/src/with-urql-client.tsx
index 9d59bb30..a16de511 100644
--- a/packages/next-urql/src/with-urql-client.tsx
+++ b/packages/next-urql/src/with-urql-client.tsx
@@ -2,29 +2,22 @@ import React from 'react';
 import { NextPage, NextPageContext } from 'next';
 import NextApp, { AppContext } from 'next/app';
 import ssrPrepass from 'react-ssr-prepass';
-import { Provider, dedupExchange, cacheExchange, fetchExchange } from 'urql';
+import {
+  Provider,
+  ssrExchange,
+  dedupExchange,
+  cacheExchange,
+  fetchExchange,
+} from 'urql';
 
 import { initUrqlClient } from './init-urql-client';
-import {
-  NextUrqlClientConfig,
-  MergeExchanges,
-  NextUrqlContext,
-  WithUrqlProps,
-} from './types';
+import { NextUrqlClientConfig, NextUrqlContext, WithUrqlProps } from './types';
 
 function getDisplayName(Component: React.ComponentType<any>) {
   return Component.displayName || Component.name || 'Component';
 }
 
-export function withUrqlClient(
-  clientConfig: NextUrqlClientConfig,
-  mergeExchanges: MergeExchanges = ssrExchange => [
-    dedupExchange,
-    cacheExchange,
-    ssrExchange,
-    fetchExchange,
-  ]
-) {
+export function withUrqlClient(getClientConfig: NextUrqlClientConfig) {
   return (AppOrPage: NextPage<any> | typeof NextApp) => {
     const withUrql = ({ urqlClient, urqlState, ...rest }: WithUrqlProps) => {
       // eslint-disable-next-line react-hooks/rules-of-hooks
@@ -33,11 +26,20 @@ export function withUrqlClient(
           return urqlClient;
         }
 
-        const clientOptions =
-          typeof clientConfig === 'function' ? clientConfig() : clientConfig;
+        const ssr = ssrExchange({ initialState: urqlState });
+        const clientConfig = getClientConfig(ssr);
+        if (!clientConfig.exchanges) {
+          // When the user does not provide exchanges we make the default assumption.
+          clientConfig.exchanges = [
+            dedupExchange,
+            cacheExchange,
+            ssr,
+            fetchExchange,
+          ];
+        }
 
         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        return initUrqlClient(clientOptions, mergeExchanges, urqlState)[0]!;
+        return initUrqlClient(clientConfig)!;
       }, [urqlClient, urqlState]);
 
       return (
@@ -59,9 +61,18 @@ export function withUrqlClient(
         ? (appOrPageCtx as AppContext).ctx
         : (appOrPageCtx as NextPageContext);
 
-      const opts =
-        typeof clientConfig === 'function' ? clientConfig(ctx) : clientConfig;
-      const [urqlClient, ssrCache] = initUrqlClient(opts, mergeExchanges);
+      const ssrCache = ssrExchange({ initialState: undefined });
+      const clientConfig = getClientConfig(ssrCache, ctx);
+      if (!clientConfig.exchanges) {
+        // When the user does not provide exchanges we make the default assumption.
+        clientConfig.exchanges = [
+          dedupExchange,
+          cacheExchange,
+          ssrCache,
+          fetchExchange,
+        ];
+      }
+      const urqlClient = initUrqlClient(clientConfig);
 
       if (urqlClient) {
         (ctx as NextUrqlContext).urqlClient = urqlClient;
diff --git a/scripts/jest/setup.js b/scripts/jest/setup.js
index b84a5a5e..cd57a55c 100644
--- a/scripts/jest/setup.js
+++ b/scripts/jest/setup.js
@@ -10,8 +10,8 @@ jest.restoreAllMocks();
 const originalConsole = console;
 global.console = {
   ...originalConsole,
-  warn() { /* noop */ },
-  error(message) { throw new Error(message); }
+  warn: jest.SpyInstance = () => { /* noop */ },
+  error: jest.SpyInstance = (message) => { throw new Error(message); }
 };
 
 jest.spyOn(console, 'log');


Test patch:
diff --git a/exchanges/graphcache/src/ast/schemaPredicates.test.ts b/exchanges/graphcache/src/ast/schemaPredicates.test.ts
index 95c061e8..b1c836d0 100644
--- a/exchanges/graphcache/src/ast/schemaPredicates.test.ts
+++ b/exchanges/graphcache/src/ast/schemaPredicates.test.ts
@@ -1,4 +1,5 @@
 import { buildClientSchema } from 'graphql';
+import { mocked } from 'ts-jest/utils';
 import * as SchemaPredicates from './schemaPredicates';
 
 describe('SchemaPredicates', () => {
@@ -53,4 +54,23 @@ describe('SchemaPredicates', () => {
       SchemaPredicates.isInterfaceOfType(schema, 'Todo', 'NoTodosError')
     ).toBeFalsy();
   });
+
+  it('should throw if a requested type does not exist', () => {
+    expect(() =>
+      SchemaPredicates.isFieldNullable(schema, 'SomeInvalidType', 'complete')
+    ).toThrow(
+      'The type `SomeInvalidType` is not an object in the defined schema, but the GraphQL document is traversing it.\nhttps://bit.ly/2XbVrpR#3'
+    );
+  });
+
+  it('should warn in console if a requested field does not exist', () => {
+    expect(
+      SchemaPredicates.isFieldNullable(schema, 'Todo', 'goof')
+    ).toBeFalsy();
+
+    expect(console.warn).toBeCalledTimes(1);
+    const warnMessage = mocked(console.warn).mock.calls[0][0];
+    expect(warnMessage).toContain('The field `goof` does not exist on `Todo`');
+    expect(warnMessage).toContain('https://bit.ly/2XbVrpR#4');
+  });
 });
diff --git a/exchanges/graphcache/src/store/store.test.ts b/exchanges/graphcache/src/store/store.test.ts
index 1e578178..170ebbcb 100644
--- a/exchanges/graphcache/src/store/store.test.ts
+++ b/exchanges/graphcache/src/store/store.test.ts
@@ -1,5 +1,5 @@
 import gql from 'graphql-tag';
-
+import { mocked } from 'ts-jest/utils';
 import { Data, StorageAdapter } from '../types';
 import { query } from '../operations/query';
 import { write, writeOptimistic } from '../operations/write';
@@ -108,6 +108,34 @@ describe('Store with KeyingConfig', () => {
     expect(store.keyOfEntity({ __typename: 'User' })).toBe('User:me');
     expect(store.keyOfEntity({ __typename: 'None' })).toBe(null);
   });
+
+  it('should not warn if keys do exist in the schema', function () {
+    new Store({
+      schema: require('../test-utils/simple_schema.json'),
+      keys: {
+        Todo: () => 'Todo',
+      },
+    });
+
+    expect(console.warn).not.toBeCalled();
+  });
+
+  it("should warn if a key doesn't exist in the schema", function () {
+    new Store({
+      schema: require('../test-utils/simple_schema.json'),
+      keys: {
+        Todo: () => 'todo',
+        NotInSchema: () => 'foo',
+      },
+    });
+
+    expect(console.warn).toBeCalledTimes(1);
+    const warnMessage = mocked(console.warn).mock.calls[0][0];
+    expect(warnMessage).toContain(
+      'The type `NotInSchema` is not an object in the defined schema, but the `keys` option is referencing it'
+    );
+    expect(warnMessage).toContain('https://bit.ly/2XbVrpR#20');
+  });
 });
 
 describe('Store with OptimisticMutationConfig', () => {
diff --git a/packages/next-urql/src/__tests__/init-urql-client.spec.ts b/packages/next-urql/src/__tests__/init-urql-client.spec.ts
index a442343d..e300dcf0 100644
--- a/packages/next-urql/src/__tests__/init-urql-client.spec.ts
+++ b/packages/next-urql/src/__tests__/init-urql-client.spec.ts
@@ -1,63 +1,12 @@
-import {
-  ssrExchange,
-  debugExchange,
-  dedupExchange,
-  cacheExchange,
-  fetchExchange,
-} from 'urql';
-
 import { initUrqlClient } from '../init-urql-client';
 
 describe('initUrqlClient', () => {
-  it('should return the urqlClient instance and ssrCache', () => {
-    const [urqlClient, ssrCache] = initUrqlClient({
+  it('should return the urqlClient instance', () => {
+    const urqlClient = initUrqlClient({
       url: 'http://localhost:3000',
     });
 
     expect(urqlClient).toHaveProperty('url', 'http://localhost:3000');
     expect(urqlClient).toHaveProperty('suspense', true);
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    expect(ssrCache!.toString()).toEqual(ssrExchange().toString());
-  });
-
-  it('should accept an optional mergeExchanges function to allow for exchange composition', () => {
-    const [urqlClient, ssrCache] = initUrqlClient(
-      {
-        url: 'http://localhost:3000',
-      },
-      ssrEx => [
-        debugExchange,
-        dedupExchange,
-        cacheExchange,
-        ssrEx,
-        fetchExchange,
-      ]
-    );
-
-    expect(urqlClient).toHaveProperty('url', 'http://localhost:3000');
-    expect(urqlClient).toHaveProperty('suspense', true);
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    expect(ssrCache!.toString()).toEqual(ssrExchange().toString());
-  });
-
-  it('should accept some initial state to populate the cache', () => {
-    const initialState = {
-      123: { data: { name: 'Kadabra', type: 'Psychic' } },
-      456: { data: { name: 'Butterfree', type: ['Psychic', 'Bug'] } },
-    };
-
-    const [urqlClient, ssrCache] = initUrqlClient(
-      {
-        url: 'http://localhost:3000',
-      },
-      undefined,
-      initialState
-    );
-
-    expect(urqlClient).toHaveProperty('url', 'http://localhost:3000');
-    expect(urqlClient).toHaveProperty('suspense', true);
-
-    const data = ssrCache && ssrCache.extractData();
-    expect(data).toEqual(initialState);
   });
 });
diff --git a/packages/next-urql/src/__tests__/with-urql-client.spec.tsx b/packages/next-urql/src/__tests__/with-urql-client.spec.tsx
index 38ed0a37..74afc40d 100644
--- a/packages/next-urql/src/__tests__/with-urql-client.spec.tsx
+++ b/packages/next-urql/src/__tests__/with-urql-client.spec.tsx
@@ -1,7 +1,7 @@
 import React from 'react';
 import { shallow, configure } from 'enzyme';
 import Adapter from 'enzyme-adapter-react-16';
-import { Client, defaultExchanges } from 'urql';
+import { Client } from 'urql';
 
 import { withUrqlClient, NextUrqlPageContext } from '..';
 import * as init from '../init-urql-client';
@@ -18,7 +18,6 @@ const MockAppTree: React.FC<any> = () => {
 
 describe('withUrqlClient', () => {
   const spyInitUrqlClient = jest.spyOn(init, 'initUrqlClient');
-  const mockMergeExchanges = jest.fn(() => defaultExchanges);
   let Component: any;
 
   beforeAll(() => {
@@ -27,7 +26,9 @@ describe('withUrqlClient', () => {
 
   describe('with client options', () => {
     beforeEach(() => {
-      Component = withUrqlClient({ url: 'http://localhost:3000' })(MockApp);
+      Component = withUrqlClient(() => ({ url: 'http://localhost:3000' }))(
+        MockApp
+      );
     });
 
     const mockContext: NextUrqlPageContext = {
@@ -45,6 +46,7 @@ describe('withUrqlClient', () => {
       expect(app.props().urqlClient).toBeInstanceOf(Client);
       expect(app.props().urqlClient.url).toBe('http://localhost:3000');
       expect(spyInitUrqlClient).toHaveBeenCalledTimes(1);
+      expect(spyInitUrqlClient.mock.calls[0][0].exchanges).toHaveLength(4);
     });
 
     it('should create the urql client instance server-side inside getInitialProps', async () => {
@@ -64,6 +66,7 @@ describe('withUrqlClient', () => {
   describe('with ctx callback to create client options', () => {
     // Simulate a token that might be passed in a request to the server-rendered application.
     const token = Math.random().toString(36).slice(-10);
+    let mockSsrExchange;
 
     const mockContext: NextUrqlPageContext = {
       AppTree: MockAppTree,
@@ -79,51 +82,37 @@ describe('withUrqlClient', () => {
     };
 
     beforeEach(() => {
-      Component = withUrqlClient(
-        ctx => ({
-          url: 'http://localhost:3000',
-          fetchOptions: {
-            headers: { Authorization: (ctx && ctx.req!.headers!.cookie) || '' },
-          },
-        }),
-        mockMergeExchanges
-      )(MockApp);
+      Component = withUrqlClient((ssrExchange, ctx) => ({
+        url: 'http://localhost:3000',
+        fetchOptions: {
+          headers: { Authorization: (ctx && ctx.req!.headers!.cookie) || '' },
+        },
+        exchanges: [(mockSsrExchange = ssrExchange)],
+      }))(MockApp);
     });
 
     it('should allow a user to access the ctx object from Next on the server', async () => {
       Component.getInitialProps &&
         (await Component.getInitialProps(mockContext));
-      expect(spyInitUrqlClient).toHaveBeenCalledWith(
-        {
-          url: 'http://localhost:3000',
-          fetchOptions: { headers: { Authorization: token } },
-        },
-        mockMergeExchanges
-      );
+      expect(spyInitUrqlClient).toHaveBeenCalledWith({
+        url: 'http://localhost:3000',
+        fetchOptions: { headers: { Authorization: token } },
+        exchanges: [mockSsrExchange],
+      });
     });
   });
 
-  describe('with mergeExchanges provided', () => {
+  describe('with exchanges provided', () => {
     const exchange = jest.fn(() => op => op);
 
     beforeEach(() => {
-      mockMergeExchanges.mockImplementation(() => [exchange] as any[]);
-      Component = withUrqlClient(
-        { url: 'http://localhost:3000' },
-        mockMergeExchanges
-      )(MockApp);
-    });
-
-    it('calls the user-supplied mergeExchanges function', () => {
-      const tree = shallow(<Component />);
-      const app = tree.find(MockApp);
-
-      const client = app.props().urqlClient;
-      expect(client).toBeInstanceOf(Client);
-      expect(mockMergeExchanges).toHaveBeenCalledTimes(1);
+      Component = withUrqlClient(() => ({
+        url: 'http://localhost:3000',
+        exchanges: [exchange] as any[],
+      }))(MockApp);
     });
 
-    it('uses exchanges returned from mergeExchanges', () => {
+    it('uses exchanges defined in the client config', () => {
       const tree = shallow(<Component />);
       const app = tree.find(MockApp);